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