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