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