blob: f14e7d704ea1b78a6f7948fce3c19a18a0e701f4 [file] [log] [blame]
Jerry Zhangf9c5c252017-08-16 18:07:51 -07001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.mtp;
18
19import android.media.MediaFile;
20import android.os.FileObserver;
21import android.os.storage.StorageVolume;
22import android.util.Log;
23
24import java.io.IOException;
25import java.nio.file.DirectoryIteratorException;
26import java.nio.file.DirectoryStream;
27import java.nio.file.Files;
28import java.nio.file.Path;
29import java.nio.file.Paths;
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.HashMap;
33import java.util.HashSet;
Jerry Zhang9a018742018-05-10 18:27:13 -070034import java.util.List;
Jerry Zhangf9c5c252017-08-16 18:07:51 -070035import java.util.Set;
Jerry Zhangf9c5c252017-08-16 18:07:51 -070036
37/**
38 * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of
39 * filesystem changes. As directories are listed, this class will cache the results,
40 * and send events when objects are added/removed from cached directories.
41 * {@hide}
42 */
43public class MtpStorageManager {
44 private static final String TAG = MtpStorageManager.class.getSimpleName();
45 public static boolean sDebug = false;
46
47 // Inotify flags not provided by FileObserver
48 private static final int IN_ONLYDIR = 0x01000000;
49 private static final int IN_Q_OVERFLOW = 0x00004000;
50 private static final int IN_IGNORED = 0x00008000;
51 private static final int IN_ISDIR = 0x40000000;
52
53 private class MtpObjectObserver extends FileObserver {
54 MtpObject mObject;
55
56 MtpObjectObserver(MtpObject object) {
57 super(object.getPath().toString(),
Jamese4f680e2018-07-02 17:42:07 +080058 MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR
59 | CLOSE_WRITE);
Jerry Zhangf9c5c252017-08-16 18:07:51 -070060 mObject = object;
61 }
62
63 @Override
64 public void onEvent(int event, String path) {
65 synchronized (MtpStorageManager.this) {
66 if ((event & IN_Q_OVERFLOW) != 0) {
67 // We are out of space in the inotify queue.
68 Log.e(TAG, "Received Inotify overflow event!");
69 }
70 MtpObject obj = mObject.getChild(path);
71 if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) {
72 if (sDebug)
73 Log.i(TAG, "Got inotify added event for " + path + " " + event);
74 handleAddedObject(mObject, path, (event & IN_ISDIR) != 0);
75 } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) {
76 if (obj == null) {
77 Log.w(TAG, "Object was null in event " + path);
78 return;
79 }
80 if (sDebug)
81 Log.i(TAG, "Got inotify removed event for " + path + " " + event);
82 handleRemovedObject(obj);
83 } else if ((event & IN_IGNORED) != 0) {
84 if (sDebug)
85 Log.i(TAG, "inotify for " + mObject.getPath() + " deleted");
86 if (mObject.mObserver != null)
87 mObject.mObserver.stopWatching();
88 mObject.mObserver = null;
Jamese4f680e2018-07-02 17:42:07 +080089 } else if ((event & CLOSE_WRITE) != 0) {
90 if (sDebug)
91 Log.i(TAG, "inotify for " + mObject.getPath() + " CLOSE_WRITE: " + path);
92 handleChangedObject(mObject, path);
Jerry Zhangf9c5c252017-08-16 18:07:51 -070093 } else {
94 Log.w(TAG, "Got unrecognized event " + path + " " + event);
95 }
96 }
97 }
98
99 @Override
100 public void finalize() {
101 // If the server shuts down and starts up again, the new server's observers can be
102 // invalidated by the finalize() calls of the previous server's observers.
103 // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and
104 // always call stopWatching() manually whenever an observer should be shut down.
105 }
106 }
107
108 /**
109 * Describes how the object is being acted on, to determine how events are handled.
110 */
111 private enum MtpObjectState {
112 NORMAL,
113 FROZEN, // Object is going to be modified in this session.
114 FROZEN_ADDED, // Object was frozen, and has been added.
115 FROZEN_REMOVED, // Object was frozen, and has been removed.
116 FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen.
117 FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed.
118 }
119
120 /**
121 * Describes the current operation being done on an object. Determines whether observers are
122 * created on new folders.
123 */
124 private enum MtpOperation {
125 NONE, // Any new folders not added as part of the session are immediately observed.
126 ADD, // New folders added as part of the session are immediately observed.
127 RENAME, // Renamed or moved folders are not immediately observed.
128 COPY, // Copied folders are immediately observed iff the original was.
129 DELETE, // Exists for debugging purposes only.
130 }
131
132 /** MtpObject represents either a file or directory in an associated storage. **/
133 public static class MtpObject {
134 // null for root objects
135 private MtpObject mParent;
136
137 private String mName;
138 private int mId;
139 private MtpObjectState mState;
140 private MtpOperation mOp;
141
142 private boolean mVisited;
143 private boolean mIsDir;
144
145 // null if not a directory
146 private HashMap<String, MtpObject> mChildren;
147 // null if not both a directory and visited
148 private FileObserver mObserver;
149
150 MtpObject(String name, int id, MtpObject parent, boolean isDir) {
151 mId = id;
152 mName = name;
153 mParent = parent;
154 mObserver = null;
155 mVisited = false;
156 mState = MtpObjectState.NORMAL;
157 mIsDir = isDir;
158 mOp = MtpOperation.NONE;
159
160 mChildren = mIsDir ? new HashMap<>() : null;
161 }
162
163 /** Public methods for getting object info **/
164
165 public String getName() {
166 return mName;
167 }
168
169 public int getId() {
170 return mId;
171 }
172
173 public boolean isDir() {
174 return mIsDir;
175 }
176
177 public int getFormat() {
178 return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null);
179 }
180
181 public int getStorageId() {
182 return getRoot().getId();
183 }
184
185 public long getModifiedTime() {
186 return getPath().toFile().lastModified() / 1000;
187 }
188
189 public MtpObject getParent() {
190 return mParent;
191 }
192
193 public MtpObject getRoot() {
194 return isRoot() ? this : mParent.getRoot();
195 }
196
197 public long getSize() {
198 return mIsDir ? 0 : getPath().toFile().length();
199 }
200
201 public Path getPath() {
202 return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName);
203 }
204
205 public boolean isRoot() {
206 return mParent == null;
207 }
208
209 /** For MtpStorageManager only **/
210
211 private void setName(String name) {
212 mName = name;
213 }
214
215 private void setId(int id) {
216 mId = id;
217 }
218
219 private boolean isVisited() {
220 return mVisited;
221 }
222
223 private void setParent(MtpObject parent) {
224 mParent = parent;
225 }
226
227 private void setDir(boolean dir) {
228 if (dir != mIsDir) {
229 mIsDir = dir;
230 mChildren = mIsDir ? new HashMap<>() : null;
231 }
232 }
233
234 private void setVisited(boolean visited) {
235 mVisited = visited;
236 }
237
238 private MtpObjectState getState() {
239 return mState;
240 }
241
242 private void setState(MtpObjectState state) {
243 mState = state;
244 if (mState == MtpObjectState.NORMAL)
245 mOp = MtpOperation.NONE;
246 }
247
248 private MtpOperation getOperation() {
249 return mOp;
250 }
251
252 private void setOperation(MtpOperation op) {
253 mOp = op;
254 }
255
256 private FileObserver getObserver() {
257 return mObserver;
258 }
259
260 private void setObserver(FileObserver observer) {
261 mObserver = observer;
262 }
263
264 private void addChild(MtpObject child) {
265 mChildren.put(child.getName(), child);
266 }
267
268 private MtpObject getChild(String name) {
269 return mChildren.get(name);
270 }
271
272 private Collection<MtpObject> getChildren() {
273 return mChildren.values();
274 }
275
276 private boolean exists() {
277 return getPath().toFile().exists();
278 }
279
280 private MtpObject copy(boolean recursive) {
281 MtpObject copy = new MtpObject(mName, mId, mParent, mIsDir);
282 copy.mIsDir = mIsDir;
283 copy.mVisited = mVisited;
284 copy.mState = mState;
285 copy.mChildren = mIsDir ? new HashMap<>() : null;
286 if (recursive && mIsDir) {
287 for (MtpObject child : mChildren.values()) {
288 MtpObject childCopy = child.copy(true);
289 childCopy.setParent(copy);
290 copy.addChild(childCopy);
291 }
292 }
293 return copy;
294 }
295 }
296
297 /**
298 * A class that processes generated filesystem events.
299 */
300 public static abstract class MtpNotifier {
301 /**
302 * Called when an object is added.
303 */
304 public abstract void sendObjectAdded(int id);
305
306 /**
307 * Called when an object is deleted.
308 */
309 public abstract void sendObjectRemoved(int id);
Jamese4f680e2018-07-02 17:42:07 +0800310
311 /**
312 * Called when an object info is changed.
313 */
314 public abstract void sendObjectInfoChanged(int id);
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700315 }
316
317 private MtpNotifier mMtpNotifier;
318
319 // A cache of MtpObjects. The objects in the cache are keyed by object id.
320 // The root object of each storage isn't in this map since they all have ObjectId 0.
321 // Instead, they can be found in mRoots keyed by storageId.
322 private HashMap<Integer, MtpObject> mObjects;
323
324 // A cache of the root MtpObject for each storage, keyed by storage id.
325 private HashMap<Integer, MtpObject> mRoots;
326
327 // Object and Storage ids are allocated incrementally and not to be reused.
328 private int mNextObjectId;
329 private int mNextStorageId;
330
331 // Special subdirectories. When set, only return objects rooted in these directories, and do
332 // not allow them to be modified.
333 private Set<String> mSubdirectories;
334
335 private volatile boolean mCheckConsistency;
336 private Thread mConsistencyThread;
337
338 public MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories) {
339 mMtpNotifier = notifier;
340 mSubdirectories = subdirectories;
341 mObjects = new HashMap<>();
342 mRoots = new HashMap<>();
343 mNextObjectId = 1;
344 mNextStorageId = 1;
345
346 mCheckConsistency = false; // Set to true to turn on automatic consistency checking
347 mConsistencyThread = new Thread(() -> {
348 while (mCheckConsistency) {
349 try {
350 Thread.sleep(15 * 1000);
351 } catch (InterruptedException e) {
352 return;
353 }
354 if (MtpStorageManager.this.checkConsistency()) {
355 Log.v(TAG, "Cache is consistent");
356 } else {
357 Log.w(TAG, "Cache is not consistent");
358 }
359 }
360 });
361 if (mCheckConsistency)
362 mConsistencyThread.start();
363 }
364
365 /**
366 * Clean up resources used by the storage manager.
367 */
368 public synchronized void close() {
Jerry Zhang9a018742018-05-10 18:27:13 -0700369 for (MtpObject obj : mObjects.values()) {
370 if (obj.getObserver() != null) {
371 obj.getObserver().stopWatching();
372 obj.setObserver(null);
373 }
374 }
375 for (MtpObject obj : mRoots.values()) {
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700376 if (obj.getObserver() != null) {
377 obj.getObserver().stopWatching();
378 obj.setObserver(null);
379 }
380 }
381
382 // Shut down the consistency checking thread
383 if (mCheckConsistency) {
384 mCheckConsistency = false;
385 mConsistencyThread.interrupt();
386 try {
387 mConsistencyThread.join();
388 } catch (InterruptedException e) {
389 // ignore
390 }
391 }
392 }
393
394 /**
395 * Sets the special subdirectories, which are the subdirectories of root storage that queries
396 * are restricted to. Must be done before any root storages are accessed.
397 * @param subDirs Subdirectories to set, or null to reset.
398 */
399 public synchronized void setSubdirectories(Set<String> subDirs) {
400 mSubdirectories = subDirs;
401 }
402
403 /**
404 * Allocates an MTP storage id for the given volume and add it to current roots.
405 * @param volume Storage to add.
406 * @return the associated MtpStorage
407 */
408 public synchronized MtpStorage addMtpStorage(StorageVolume volume) {
409 int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1;
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700410 MtpStorage storage = new MtpStorage(volume, storageId);
Jerry Zhang71938e12018-05-10 18:28:29 -0700411 MtpObject root = new MtpObject(storage.getPath(), storageId, null, true);
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700412 mRoots.put(storageId, root);
413 return storage;
414 }
415
416 /**
417 * Removes the given storage and all associated items from the cache.
418 * @param storage Storage to remove.
419 */
420 public synchronized void removeMtpStorage(MtpStorage storage) {
421 removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true);
422 }
423
424 /**
425 * Checks if the given object can be renamed, moved, or deleted.
426 * If there are special subdirectories, they cannot be modified.
427 * @param obj Object to check.
428 * @return Whether object can be modified.
429 */
430 private synchronized boolean isSpecialSubDir(MtpObject obj) {
431 return obj.getParent().isRoot() && mSubdirectories != null
432 && !mSubdirectories.contains(obj.getName());
433 }
434
435 /**
436 * Get the object with the specified path. Visit any necessary directories on the way.
437 * @param path Full path of the object to find.
438 * @return The desired object, or null if it cannot be found.
439 */
440 public synchronized MtpObject getByPath(String path) {
441 MtpObject obj = null;
442 for (MtpObject root : mRoots.values()) {
443 if (path.startsWith(root.getName())) {
444 obj = root;
445 path = path.substring(root.getName().length());
446 }
447 }
448 for (String name : path.split("/")) {
449 if (obj == null || !obj.isDir())
450 return null;
451 if ("".equals(name))
452 continue;
453 if (!obj.isVisited())
454 getChildren(obj);
455 obj = obj.getChild(name);
456 }
457 return obj;
458 }
459
460 /**
461 * Get the object with specified id.
462 * @param id Id of object. must not be 0 or 0xFFFFFFFF
463 * @return Object, or null if error.
464 */
465 public synchronized MtpObject getObject(int id) {
466 if (id == 0 || id == 0xFFFFFFFF) {
467 Log.w(TAG, "Can't get root storages with getObject()");
468 return null;
469 }
470 if (!mObjects.containsKey(id)) {
471 Log.w(TAG, "Id " + id + " doesn't exist");
472 return null;
473 }
474 return mObjects.get(id);
475 }
476
477 /**
478 * Get the storage with specified id.
479 * @param id Storage id.
480 * @return Object that is the root of the storage, or null if error.
481 */
482 public MtpObject getStorageRoot(int id) {
483 if (!mRoots.containsKey(id)) {
484 Log.w(TAG, "StorageId " + id + " doesn't exist");
485 return null;
486 }
487 return mRoots.get(id);
488 }
489
490 private int getNextObjectId() {
491 int ret = mNextObjectId;
492 // Treat the id as unsigned int
493 mNextObjectId = (int) ((long) mNextObjectId + 1);
494 return ret;
495 }
496
497 private int getNextStorageId() {
498 return mNextStorageId++;
499 }
500
501 /**
502 * Get all objects matching the given parent, format, and storage
503 * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root
504 * @param format format of returned objects. 0 for any format
505 * @param storageId storage id to look in. 0xFFFFFFFF for all storages
Jerry Zhang9a018742018-05-10 18:27:13 -0700506 * @return A list of matched objects, or null if error
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700507 */
Jerry Zhang9a018742018-05-10 18:27:13 -0700508 public synchronized List<MtpObject> getObjects(int parent, int format, int storageId) {
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700509 boolean recursive = parent == 0;
Jerry Zhang9a018742018-05-10 18:27:13 -0700510 ArrayList<MtpObject> objs = new ArrayList<>();
511 boolean ret = true;
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700512 if (parent == 0xFFFFFFFF)
513 parent = 0;
514 if (storageId == 0xFFFFFFFF) {
515 // query all stores
516 if (parent == 0) {
517 // Get the objects of this format and parent in each store.
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700518 for (MtpObject root : mRoots.values()) {
Jerry Zhang9a018742018-05-10 18:27:13 -0700519 ret &= getObjects(objs, root, format, recursive);
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700520 }
Jerry Zhang9a018742018-05-10 18:27:13 -0700521 return ret ? objs : null;
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700522 }
523 }
524 MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent);
525 if (obj == null)
526 return null;
Jerry Zhang9a018742018-05-10 18:27:13 -0700527 ret = getObjects(objs, obj, format, recursive);
528 return ret ? objs : null;
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700529 }
530
Jerry Zhang9a018742018-05-10 18:27:13 -0700531 private synchronized boolean getObjects(List<MtpObject> toAdd, MtpObject parent, int format, boolean rec) {
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700532 Collection<MtpObject> children = getChildren(parent);
533 if (children == null)
Jerry Zhang9a018742018-05-10 18:27:13 -0700534 return false;
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700535
Jerry Zhang9a018742018-05-10 18:27:13 -0700536 for (MtpObject o : children) {
537 if (format == 0 || o.getFormat() == format) {
538 toAdd.add(o);
539 }
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700540 }
Jerry Zhang9a018742018-05-10 18:27:13 -0700541 boolean ret = true;
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700542 if (rec) {
543 // Get all objects recursively.
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700544 for (MtpObject o : children) {
545 if (o.isDir())
Jerry Zhang9a018742018-05-10 18:27:13 -0700546 ret &= getObjects(toAdd, o, format, true);
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700547 }
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700548 }
549 return ret;
550 }
551
552 /**
553 * Return the children of the given object. If the object hasn't been visited yet, add
554 * its children to the cache and start observing it.
555 * @param object the parent object
556 * @return The collection of child objects or null if error
557 */
558 private synchronized Collection<MtpObject> getChildren(MtpObject object) {
559 if (object == null || !object.isDir()) {
560 Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId()));
561 return null;
562 }
563 if (!object.isVisited()) {
564 Path dir = object.getPath();
565 /*
566 * If a file is added after the observer starts watching the directory, but before
567 * the contents are listed, it will generate an event that will get processed
568 * after this synchronized function returns. We handle this by ignoring object
569 * added events if an object at that path already exists.
570 */
571 if (object.getObserver() != null)
572 Log.e(TAG, "Observer is not null!");
573 object.setObserver(new MtpObjectObserver(object));
574 object.getObserver().startWatching();
575 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
576 for (Path file : stream) {
577 addObjectToCache(object, file.getFileName().toString(),
578 file.toFile().isDirectory());
579 }
580 } catch (IOException | DirectoryIteratorException e) {
581 Log.e(TAG, e.toString());
582 object.getObserver().stopWatching();
583 object.setObserver(null);
584 return null;
585 }
586 object.setVisited(true);
587 }
588 return object.getChildren();
589 }
590
591 /**
592 * Create a new object from the given path and add it to the cache.
593 * @param parent The parent object
594 * @param newName Path of the new object
595 * @return the new object if success, else null
596 */
597 private synchronized MtpObject addObjectToCache(MtpObject parent, String newName,
598 boolean isDir) {
599 if (!parent.isRoot() && getObject(parent.getId()) != parent)
600 // parent object has been removed
601 return null;
602 if (parent.getChild(newName) != null) {
603 // Object already exists
604 return null;
605 }
606 if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) {
607 // Not one of the restricted subdirectories.
608 return null;
609 }
610
611 MtpObject obj = new MtpObject(newName, getNextObjectId(), parent, isDir);
612 mObjects.put(obj.getId(), obj);
613 parent.addChild(obj);
614 return obj;
615 }
616
617 /**
618 * Remove the given path from the cache.
619 * @param removed The removed object
620 * @param removeGlobal Whether to remove the object from the global id map
621 * @param recursive Whether to also remove its children recursively.
622 * @return true if successfully removed
623 */
624 private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal,
625 boolean recursive) {
626 boolean ret = removed.isRoot()
627 || removed.getParent().mChildren.remove(removed.getName(), removed);
628 if (!ret && sDebug)
629 Log.w(TAG, "Failed to remove from parent " + removed.getPath());
630 if (removed.isRoot()) {
631 ret = mRoots.remove(removed.getId(), removed) && ret;
632 } else if (removeGlobal) {
633 ret = mObjects.remove(removed.getId(), removed) && ret;
634 }
635 if (!ret && sDebug)
636 Log.w(TAG, "Failed to remove from global cache " + removed.getPath());
637 if (removed.getObserver() != null) {
638 removed.getObserver().stopWatching();
639 removed.setObserver(null);
640 }
641 if (removed.isDir() && recursive) {
642 // Remove all descendants from cache recursively
643 Collection<MtpObject> children = new ArrayList<>(removed.getChildren());
644 for (MtpObject child : children) {
645 ret = removeObjectFromCache(child, removeGlobal, true) && ret;
646 }
647 }
648 return ret;
649 }
650
651 private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) {
652 MtpOperation op = MtpOperation.NONE;
653 MtpObject obj = parent.getChild(path);
654 if (obj != null) {
655 MtpObjectState state = obj.getState();
656 op = obj.getOperation();
657 if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED)
658 Log.d(TAG, "Inconsistent directory info! " + obj.getPath());
659 obj.setDir(isDir);
660 switch (state) {
661 case FROZEN:
662 case FROZEN_REMOVED:
663 obj.setState(MtpObjectState.FROZEN_ADDED);
664 break;
665 case FROZEN_ONESHOT_ADD:
666 obj.setState(MtpObjectState.NORMAL);
667 break;
668 case NORMAL:
669 case FROZEN_ADDED:
670 // This can happen when handling listed object in a new directory.
671 return;
672 default:
673 Log.w(TAG, "Unexpected state in add " + path + " " + state);
674 }
675 if (sDebug)
676 Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
677 } else {
678 obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir);
679 if (obj != null) {
680 MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId());
681 } else {
682 if (sDebug)
683 Log.w(TAG, "object " + path + " already exists");
684 return;
685 }
686 }
687 if (isDir) {
688 // If this was added as part of a rename do not visit or send events.
689 if (op == MtpOperation.RENAME)
690 return;
691
692 // If it was part of a copy operation, then only add observer if it was visited before.
693 if (op == MtpOperation.COPY && !obj.isVisited())
694 return;
695
696 if (obj.getObserver() != null) {
697 Log.e(TAG, "Observer is not null!");
698 return;
699 }
700 obj.setObserver(new MtpObjectObserver(obj));
701 obj.getObserver().startWatching();
702 obj.setVisited(true);
703
704 // It's possible that objects were added to a watched directory before the watch can be
705 // created, so manually handle those.
706 try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
707 for (Path file : stream) {
708 if (sDebug)
709 Log.i(TAG, "Manually handling event for " + file.getFileName().toString());
710 handleAddedObject(obj, file.getFileName().toString(),
711 file.toFile().isDirectory());
712 }
713 } catch (IOException | DirectoryIteratorException e) {
714 Log.e(TAG, e.toString());
715 obj.getObserver().stopWatching();
716 obj.setObserver(null);
717 }
718 }
719 }
720
721 private synchronized void handleRemovedObject(MtpObject obj) {
722 MtpObjectState state = obj.getState();
723 MtpOperation op = obj.getOperation();
724 switch (state) {
725 case FROZEN_ADDED:
726 obj.setState(MtpObjectState.FROZEN_REMOVED);
727 break;
728 case FROZEN_ONESHOT_DEL:
729 removeObjectFromCache(obj, op != MtpOperation.RENAME, false);
730 break;
731 case FROZEN:
732 obj.setState(MtpObjectState.FROZEN_REMOVED);
733 break;
734 case NORMAL:
735 if (MtpStorageManager.this.removeObjectFromCache(obj, true, true))
736 MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId());
737 break;
738 default:
739 // This shouldn't happen; states correspond to objects that don't exist
740 Log.e(TAG, "Got unexpected object remove for " + obj.getName());
741 }
742 if (sDebug)
743 Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
744 }
745
Jamese4f680e2018-07-02 17:42:07 +0800746 private synchronized void handleChangedObject(MtpObject parent, String path) {
747 MtpOperation op = MtpOperation.NONE;
748 MtpObject obj = parent.getChild(path);
749 if (obj != null) {
750 // Only handle files for size change notification event
751 if ((!obj.isDir()) && (obj.getSize() > 0))
752 {
753 MtpObjectState state = obj.getState();
754 op = obj.getOperation();
755 MtpStorageManager.this.mMtpNotifier.sendObjectInfoChanged(obj.getId());
756 if (sDebug)
757 Log.d(TAG, "sendObjectInfoChanged: id=" + obj.getId() + ",size=" + obj.getSize());
758 }
759 } else {
760 if (sDebug)
761 Log.w(TAG, "object " + path + " null");
762 }
763 }
764
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700765 /**
766 * Block the caller until all events currently in the event queue have been
767 * read and processed. Used for testing purposes.
768 */
769 public void flushEvents() {
770 try {
771 // TODO make this smarter
772 Thread.sleep(500);
773 } catch (InterruptedException e) {
774
775 }
776 }
777
778 /**
779 * Dumps a representation of the cache to log.
780 */
781 public synchronized void dump() {
782 for (int key : mObjects.keySet()) {
783 MtpObject obj = mObjects.get(key);
784 Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null")
785 + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj")
786 + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState());
787 }
788 }
789
790 /**
791 * Checks consistency of the cache. This checks whether all objects have correct links
792 * to their parent, and whether directories are missing or have extraneous objects.
793 * @return true iff cache is consistent
794 */
795 public synchronized boolean checkConsistency() {
Jerry Zhang9a018742018-05-10 18:27:13 -0700796 List<MtpObject> objs = new ArrayList<>();
797 objs.addAll(mRoots.values());
798 objs.addAll(mObjects.values());
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700799 boolean ret = true;
Jerry Zhang9a018742018-05-10 18:27:13 -0700800 for (MtpObject obj : objs) {
Jerry Zhangf9c5c252017-08-16 18:07:51 -0700801 if (!obj.exists()) {
802 Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId());
803 ret = false;
804 }
805 if (obj.getState() != MtpObjectState.NORMAL) {
806 Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState());
807 ret = false;
808 }
809 if (obj.getOperation() != MtpOperation.NONE) {
810 Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation());
811 ret = false;
812 }
813 if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) {
814 Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly");
815 ret = false;
816 }
817 if (obj.getParent() != null) {
818 if (obj.getParent().isRoot() && obj.getParent()
819 != mRoots.get(obj.getParent().getId())) {
820 Log.w(TAG, "Root parent is not in root mapping " + obj.getPath());
821 ret = false;
822 }
823 if (!obj.getParent().isRoot() && obj.getParent()
824 != mObjects.get(obj.getParent().getId())) {
825 Log.w(TAG, "Parent is not in object mapping " + obj.getPath());
826 ret = false;
827 }
828 if (obj.getParent().getChild(obj.getName()) != obj) {
829 Log.w(TAG, "Child does not exist in parent " + obj.getPath());
830 ret = false;
831 }
832 }
833 if (obj.isDir()) {
834 if (obj.isVisited() == (obj.getObserver() == null)) {
835 Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ")
836 + " visited but observer is " + obj.getObserver());
837 ret = false;
838 }
839 if (!obj.isVisited() && obj.getChildren().size() > 0) {
840 Log.w(TAG, obj.getPath() + " is not visited but has children");
841 ret = false;
842 }
843 try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
844 Set<String> files = new HashSet<>();
845 for (Path file : stream) {
846 if (obj.isVisited() &&
847 obj.getChild(file.getFileName().toString()) == null &&
848 (mSubdirectories == null || !obj.isRoot() ||
849 mSubdirectories.contains(file.getFileName().toString()))) {
850 Log.w(TAG, "File exists in fs but not in children " + file);
851 ret = false;
852 }
853 files.add(file.toString());
854 }
855 for (MtpObject child : obj.getChildren()) {
856 if (!files.contains(child.getPath().toString())) {
857 Log.w(TAG, "File in children doesn't exist in fs " + child.getPath());
858 ret = false;
859 }
860 if (child != mObjects.get(child.getId())) {
861 Log.w(TAG, "Child is not in object map " + child.getPath());
862 ret = false;
863 }
864 }
865 } catch (IOException | DirectoryIteratorException e) {
866 Log.w(TAG, e.toString());
867 ret = false;
868 }
869 }
870 }
871 return ret;
872 }
873
874 /**
875 * Informs MtpStorageManager that an object with the given path is about to be added.
876 * @param parent The parent object of the object to be added.
877 * @param name Filename of object to add.
878 * @return Object id of the added object, or -1 if it cannot be added.
879 */
880 public synchronized int beginSendObject(MtpObject parent, String name, int format) {
881 if (sDebug)
882 Log.v(TAG, "beginSendObject " + name);
883 if (!parent.isDir())
884 return -1;
885 if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
886 return -1;
887 getChildren(parent); // Ensure parent is visited
888 MtpObject obj = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION);
889 if (obj == null)
890 return -1;
891 obj.setState(MtpObjectState.FROZEN);
892 obj.setOperation(MtpOperation.ADD);
893 return obj.getId();
894 }
895
896 /**
897 * Clean up the object state after a sendObject operation.
898 * @param obj The object, returned from beginAddObject().
899 * @param succeeded Whether the file was successfully created.
900 * @return Whether cache state was successfully cleaned up.
901 */
902 public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) {
903 if (sDebug)
904 Log.v(TAG, "endSendObject " + succeeded);
905 return generalEndAddObject(obj, succeeded, true);
906 }
907
908 /**
909 * Informs MtpStorageManager that the given object is about to be renamed.
910 * If this returns true, it must be followed with an endRenameObject()
911 * @param obj Object to be renamed.
912 * @param newName New name of the object.
913 * @return Whether renaming is allowed.
914 */
915 public synchronized boolean beginRenameObject(MtpObject obj, String newName) {
916 if (sDebug)
917 Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName);
918 if (obj.isRoot())
919 return false;
920 if (isSpecialSubDir(obj))
921 return false;
922 if (obj.getParent().getChild(newName) != null)
923 // Object already exists in parent with that name.
924 return false;
925
926 MtpObject oldObj = obj.copy(false);
927 obj.setName(newName);
928 obj.getParent().addChild(obj);
929 oldObj.getParent().addChild(oldObj);
930 return generalBeginRenameObject(oldObj, obj);
931 }
932
933 /**
934 * Cleans up cache state after a rename operation and sends any events that were missed.
935 * @param obj The object being renamed, the same one that was passed in beginRenameObject().
936 * @param oldName The previous name of the object.
937 * @param success Whether the rename operation succeeded.
938 * @return Whether state was successfully cleaned up.
939 */
940 public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) {
941 if (sDebug)
942 Log.v(TAG, "endRenameObject " + success);
943 MtpObject parent = obj.getParent();
944 MtpObject oldObj = parent.getChild(oldName);
945 if (!success) {
946 // If the rename failed, we want oldObj to be the original and obj to be the dummy.
947 // Switch the objects, except for their name and state.
948 MtpObject temp = oldObj;
949 MtpObjectState oldState = oldObj.getState();
950 temp.setName(obj.getName());
951 temp.setState(obj.getState());
952 oldObj = obj;
953 oldObj.setName(oldName);
954 oldObj.setState(oldState);
955 obj = temp;
956 parent.addChild(obj);
957 parent.addChild(oldObj);
958 }
959 return generalEndRenameObject(oldObj, obj, success);
960 }
961
962 /**
963 * Informs MtpStorageManager that the given object is about to be deleted by the initiator,
964 * so don't send an event.
965 * @param obj Object to be deleted.
966 * @return Whether cache deletion is allowed.
967 */
968 public synchronized boolean beginRemoveObject(MtpObject obj) {
969 if (sDebug)
970 Log.v(TAG, "beginRemoveObject " + obj.getName());
971 return !obj.isRoot() && !isSpecialSubDir(obj)
972 && generalBeginRemoveObject(obj, MtpOperation.DELETE);
973 }
974
975 /**
976 * Clean up cache state after a delete operation and send any events that were missed.
977 * @param obj Object to be deleted, same one passed in beginRemoveObject().
978 * @param success Whether operation was completed successfully.
979 * @return Whether cache state is correct.
980 */
981 public synchronized boolean endRemoveObject(MtpObject obj, boolean success) {
982 if (sDebug)
983 Log.v(TAG, "endRemoveObject " + success);
984 boolean ret = true;
985 if (obj.isDir()) {
986 for (MtpObject child : new ArrayList<>(obj.getChildren()))
987 if (child.getOperation() == MtpOperation.DELETE)
988 ret = endRemoveObject(child, success) && ret;
989 }
990 return generalEndRemoveObject(obj, success, true) && ret;
991 }
992
993 /**
994 * Informs MtpStorageManager that the given object is about to be moved to a new parent.
995 * @param obj Object to be moved.
996 * @param newParent The new parent object.
997 * @return Whether the move is allowed.
998 */
999 public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) {
1000 if (sDebug)
1001 Log.v(TAG, "beginMoveObject " + newParent.getPath());
1002 if (obj.isRoot())
1003 return false;
1004 if (isSpecialSubDir(obj))
1005 return false;
1006 getChildren(newParent); // Ensure parent is visited
1007 if (newParent.getChild(obj.getName()) != null)
1008 // Object already exists in parent with that name.
1009 return false;
1010 if (obj.getStorageId() != newParent.getStorageId()) {
1011 /*
1012 * The move is occurring across storages. The observers will not remain functional
1013 * after the move, and the move will not be atomic. We have to copy the file tree
1014 * to the destination and recreate the observers once copy is complete.
1015 */
1016 MtpObject newObj = obj.copy(true);
1017 newObj.setParent(newParent);
1018 newParent.addChild(newObj);
1019 return generalBeginRemoveObject(obj, MtpOperation.RENAME)
1020 && generalBeginCopyObject(newObj, false);
1021 }
1022 // Move obj to new parent, create a dummy object in the old parent.
1023 MtpObject oldObj = obj.copy(false);
1024 obj.setParent(newParent);
1025 oldObj.getParent().addChild(oldObj);
1026 obj.getParent().addChild(obj);
1027 return generalBeginRenameObject(oldObj, obj);
1028 }
1029
1030 /**
1031 * Clean up cache state after a move operation and send any events that were missed.
1032 * @param oldParent The old parent object.
1033 * @param newParent The new parent object.
1034 * @param name The name of the object being moved.
1035 * @param success Whether operation was completed successfully.
1036 * @return Whether cache state is correct.
1037 */
1038 public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name,
1039 boolean success) {
1040 if (sDebug)
1041 Log.v(TAG, "endMoveObject " + success);
1042 MtpObject oldObj = oldParent.getChild(name);
1043 MtpObject newObj = newParent.getChild(name);
1044 if (oldObj == null || newObj == null)
1045 return false;
1046 if (oldParent.getStorageId() != newObj.getStorageId()) {
1047 boolean ret = endRemoveObject(oldObj, success);
1048 return generalEndCopyObject(newObj, success, true) && ret;
1049 }
1050 if (!success) {
1051 // If the rename failed, we want oldObj to be the original and obj to be the dummy.
1052 // Switch the objects, except for their parent and state.
1053 MtpObject temp = oldObj;
1054 MtpObjectState oldState = oldObj.getState();
1055 temp.setParent(newObj.getParent());
1056 temp.setState(newObj.getState());
1057 oldObj = newObj;
1058 oldObj.setParent(oldParent);
1059 oldObj.setState(oldState);
1060 newObj = temp;
1061 newObj.getParent().addChild(newObj);
1062 oldParent.addChild(oldObj);
1063 }
1064 return generalEndRenameObject(oldObj, newObj, success);
1065 }
1066
1067 /**
1068 * Informs MtpStorageManager that the given object is about to be copied recursively.
1069 * @param object Object to be copied
1070 * @param newParent New parent for the object.
1071 * @return The object id for the new copy, or -1 if error.
1072 */
1073 public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) {
1074 if (sDebug)
1075 Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath());
1076 String name = object.getName();
1077 if (!newParent.isDir())
1078 return -1;
1079 if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
1080 return -1;
1081 getChildren(newParent); // Ensure parent is visited
1082 if (newParent.getChild(name) != null)
1083 return -1;
1084 MtpObject newObj = object.copy(object.isDir());
1085 newParent.addChild(newObj);
1086 newObj.setParent(newParent);
1087 if (!generalBeginCopyObject(newObj, true))
1088 return -1;
1089 return newObj.getId();
1090 }
1091
1092 /**
1093 * Cleans up cache state after a copy operation.
1094 * @param object Object that was copied.
1095 * @param success Whether the operation was successful.
1096 * @return Whether cache state is consistent.
1097 */
1098 public synchronized boolean endCopyObject(MtpObject object, boolean success) {
1099 if (sDebug)
1100 Log.v(TAG, "endCopyObject " + object.getName() + " " + success);
1101 return generalEndCopyObject(object, success, false);
1102 }
1103
1104 private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded,
1105 boolean removeGlobal) {
1106 switch (obj.getState()) {
1107 case FROZEN:
1108 // Object was never created.
1109 if (succeeded) {
1110 // The operation was successful so the event must still be in the queue.
1111 obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD);
1112 } else {
1113 // The operation failed and never created the file.
1114 if (!removeObjectFromCache(obj, removeGlobal, false)) {
1115 return false;
1116 }
1117 }
1118 break;
1119 case FROZEN_ADDED:
1120 obj.setState(MtpObjectState.NORMAL);
1121 if (!succeeded) {
1122 MtpObject parent = obj.getParent();
1123 // The operation failed but some other process created the file. Send an event.
1124 if (!removeObjectFromCache(obj, removeGlobal, false))
1125 return false;
1126 handleAddedObject(parent, obj.getName(), obj.isDir());
1127 }
1128 // else: The operation successfully created the object.
1129 break;
1130 case FROZEN_REMOVED:
1131 if (!removeObjectFromCache(obj, removeGlobal, false))
1132 return false;
1133 if (succeeded) {
1134 // Some other process deleted the object. Send an event.
1135 mMtpNotifier.sendObjectRemoved(obj.getId());
1136 }
1137 // else: Mtp deleted the object as part of cleanup. Don't send an event.
1138 break;
1139 default:
1140 return false;
1141 }
1142 return true;
1143 }
1144
1145 private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success,
1146 boolean removeGlobal) {
1147 switch (obj.getState()) {
1148 case FROZEN:
1149 if (success) {
1150 // Object was deleted successfully, and event is still in the queue.
1151 obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL);
1152 } else {
1153 // Object was not deleted.
1154 obj.setState(MtpObjectState.NORMAL);
1155 }
1156 break;
1157 case FROZEN_ADDED:
1158 // Object was deleted, and then readded.
1159 obj.setState(MtpObjectState.NORMAL);
1160 if (success) {
1161 // Some other process readded the object.
1162 MtpObject parent = obj.getParent();
1163 if (!removeObjectFromCache(obj, removeGlobal, false))
1164 return false;
1165 handleAddedObject(parent, obj.getName(), obj.isDir());
1166 }
1167 // else : Object still exists after failure.
1168 break;
1169 case FROZEN_REMOVED:
1170 if (!removeObjectFromCache(obj, removeGlobal, false))
1171 return false;
1172 if (!success) {
1173 // Some other process deleted the object.
1174 mMtpNotifier.sendObjectRemoved(obj.getId());
1175 }
1176 // else : This process deleted the object as part of the operation.
1177 break;
1178 default:
1179 return false;
1180 }
1181 return true;
1182 }
1183
1184 private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) {
1185 fromObj.setState(MtpObjectState.FROZEN);
1186 toObj.setState(MtpObjectState.FROZEN);
1187 fromObj.setOperation(MtpOperation.RENAME);
1188 toObj.setOperation(MtpOperation.RENAME);
1189 return true;
1190 }
1191
1192 private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj,
1193 boolean success) {
1194 boolean ret = generalEndRemoveObject(fromObj, success, !success);
1195 return generalEndAddObject(toObj, success, success) && ret;
1196 }
1197
1198 private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) {
1199 obj.setState(MtpObjectState.FROZEN);
1200 obj.setOperation(op);
1201 if (obj.isDir()) {
1202 for (MtpObject child : obj.getChildren())
1203 generalBeginRemoveObject(child, op);
1204 }
1205 return true;
1206 }
1207
1208 private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) {
1209 obj.setState(MtpObjectState.FROZEN);
1210 obj.setOperation(MtpOperation.COPY);
1211 if (newId) {
1212 obj.setId(getNextObjectId());
1213 mObjects.put(obj.getId(), obj);
1214 }
1215 if (obj.isDir())
1216 for (MtpObject child : obj.getChildren())
1217 if (!generalBeginCopyObject(child, newId))
1218 return false;
1219 return true;
1220 }
1221
1222 private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) {
1223 if (success && addGlobal)
1224 mObjects.put(obj.getId(), obj);
1225 boolean ret = true;
1226 if (obj.isDir()) {
1227 for (MtpObject child : new ArrayList<>(obj.getChildren())) {
1228 if (child.getOperation() == MtpOperation.COPY)
1229 ret = generalEndCopyObject(child, success, addGlobal) && ret;
1230 }
1231 }
1232 ret = generalEndAddObject(obj, success, success || !addGlobal) && ret;
1233 return ret;
1234 }
1235}