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