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