Home | History | Annotate | Download | only in mtp
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.mtp;
     18 
     19 import android.media.MediaFile;
     20 import android.os.FileObserver;
     21 import android.os.storage.StorageVolume;
     22 import android.util.Log;
     23 
     24 import java.io.IOException;
     25 import java.nio.file.DirectoryIteratorException;
     26 import java.nio.file.DirectoryStream;
     27 import java.nio.file.Files;
     28 import java.nio.file.Path;
     29 import java.nio.file.Paths;
     30 import java.util.ArrayList;
     31 import java.util.Collection;
     32 import java.util.HashMap;
     33 import java.util.HashSet;
     34 import java.util.Iterator;
     35 import java.util.Objects;
     36 import java.util.Set;
     37 import java.util.stream.Stream;
     38 
     39 /**
     40  * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of
     41  * filesystem changes. As directories are listed, this class will cache the results,
     42  * and send events when objects are added/removed from cached directories.
     43  * {@hide}
     44  */
     45 public class MtpStorageManager {
     46     private static final String TAG = MtpStorageManager.class.getSimpleName();
     47     public static boolean sDebug = false;
     48 
     49     // Inotify flags not provided by FileObserver
     50     private static final int IN_ONLYDIR = 0x01000000;
     51     private static final int IN_Q_OVERFLOW = 0x00004000;
     52     private static final int IN_IGNORED    = 0x00008000;
     53     private static final int IN_ISDIR = 0x40000000;
     54 
     55     private class MtpObjectObserver extends FileObserver {
     56         MtpObject mObject;
     57 
     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