| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.mtp; |
| |
| import android.media.MediaFile; |
| import android.os.FileObserver; |
| import android.os.SystemProperties; |
| import android.os.storage.StorageVolume; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.system.StructStat; |
| import android.util.Log; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.io.IOException; |
| import java.nio.file.DirectoryIteratorException; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Supplier; |
| |
| /** |
| * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of |
| * filesystem changes. As directories are listed, this class will cache the results, |
| * and send events when objects are added/removed from cached directories. |
| * {@hide} |
| */ |
| public class MtpStorageManager { |
| private static final String TAG = MtpStorageManager.class.getSimpleName(); |
| public static boolean sDebug = false; |
| |
| // Inotify flags not provided by FileObserver |
| private static final int IN_ONLYDIR = 0x01000000; |
| private static final int IN_Q_OVERFLOW = 0x00004000; |
| private static final int IN_IGNORED = 0x00008000; |
| private static final int IN_ISDIR = 0x40000000; |
| |
| private class MtpObjectObserver extends FileObserver { |
| MtpObject mObject; |
| |
| MtpObjectObserver(MtpObject object) { |
| super(object.getPath().toString(), |
| MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR |
| | CLOSE_WRITE); |
| mObject = object; |
| } |
| |
| @Override |
| public void onEvent(int event, String path) { |
| synchronized (MtpStorageManager.this) { |
| if ((event & IN_Q_OVERFLOW) != 0) { |
| // We are out of space in the inotify queue. |
| Log.e(TAG, "Received Inotify overflow event!"); |
| } |
| MtpObject obj = mObject.getChild(path); |
| if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) { |
| if (sDebug) |
| Log.i(TAG, "Got inotify added event for " + path + " " + event); |
| handleAddedObject(mObject, path, (event & IN_ISDIR) != 0); |
| } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) { |
| if (obj == null) { |
| Log.w(TAG, "Object was null in event " + path); |
| return; |
| } |
| if (sDebug) |
| Log.i(TAG, "Got inotify removed event for " + path + " " + event); |
| handleRemovedObject(obj); |
| } else if ((event & IN_IGNORED) != 0) { |
| if (sDebug) |
| Log.i(TAG, "inotify for " + mObject.getPath() + " deleted"); |
| if (mObject.mObserver != null) |
| mObject.mObserver.stopWatching(); |
| mObject.mObserver = null; |
| } else if ((event & CLOSE_WRITE) != 0) { |
| if (sDebug) |
| Log.i(TAG, "inotify for " + mObject.getPath() + " CLOSE_WRITE: " + path); |
| handleChangedObject(mObject, path); |
| } else { |
| Log.w(TAG, "Got unrecognized event " + path + " " + event); |
| } |
| } |
| } |
| |
| @Override |
| public void finalize() { |
| // If the server shuts down and starts up again, the new server's observers can be |
| // invalidated by the finalize() calls of the previous server's observers. |
| // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and |
| // always call stopWatching() manually whenever an observer should be shut down. |
| } |
| } |
| |
| /** |
| * Describes how the object is being acted on, to determine how events are handled. |
| */ |
| private enum MtpObjectState { |
| NORMAL, |
| FROZEN, // Object is going to be modified in this session. |
| FROZEN_ADDED, // Object was frozen, and has been added. |
| FROZEN_REMOVED, // Object was frozen, and has been removed. |
| FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen. |
| FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed. |
| } |
| |
| /** |
| * Describes the current operation being done on an object. Determines whether observers are |
| * created on new folders. |
| */ |
| private enum MtpOperation { |
| NONE, // Any new folders not added as part of the session are immediately observed. |
| ADD, // New folders added as part of the session are immediately observed. |
| RENAME, // Renamed or moved folders are not immediately observed. |
| COPY, // Copied folders are immediately observed iff the original was. |
| DELETE, // Exists for debugging purposes only. |
| } |
| |
| /** MtpObject represents either a file or directory in an associated storage. **/ |
| public static class MtpObject { |
| private MtpStorage mStorage; |
| // null for root objects |
| private MtpObject mParent; |
| |
| private String mName; |
| private int mId; |
| private MtpObjectState mState; |
| private MtpOperation mOp; |
| |
| private boolean mVisited; |
| private boolean mIsDir; |
| |
| // null if not a directory |
| private HashMap<String, MtpObject> mChildren; |
| // null if not both a directory and visited |
| private FileObserver mObserver; |
| |
| MtpObject(String name, int id, MtpStorage storage, MtpObject parent, boolean isDir) { |
| mId = id; |
| mName = name; |
| mStorage = Preconditions.checkNotNull(storage); |
| mParent = parent; |
| mObserver = null; |
| mVisited = false; |
| mState = MtpObjectState.NORMAL; |
| mIsDir = isDir; |
| mOp = MtpOperation.NONE; |
| |
| mChildren = mIsDir ? new HashMap<>() : null; |
| } |
| |
| /** Public methods for getting object info **/ |
| |
| public String getName() { |
| return mName; |
| } |
| |
| public int getId() { |
| return mId; |
| } |
| |
| public boolean isDir() { |
| return mIsDir; |
| } |
| |
| public int getFormat() { |
| return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null); |
| } |
| |
| public int getStorageId() { |
| return getRoot().getId(); |
| } |
| |
| public long getModifiedTime() { |
| return getPath().toFile().lastModified() / 1000; |
| } |
| |
| public MtpObject getParent() { |
| return mParent; |
| } |
| |
| public MtpObject getRoot() { |
| return isRoot() ? this : mParent.getRoot(); |
| } |
| |
| public long getSize() { |
| return mIsDir ? 0 : maybeApplyTranscodeLengthWorkaround(getPath().toFile().length()); |
| } |
| |
| private long maybeApplyTranscodeLengthWorkaround(long length) { |
| // Windows truncates transferred files to the size advertised in the object property. |
| if (mStorage.isHostWindows() && isTranscodeMtpEnabled() && isFileTranscodeSupported()) { |
| // If the file supports transcoding, we double the returned size to accommodate |
| // the increase in size from transcoding to AVC. This is the same heuristic |
| // applied in the FUSE daemon (MediaProvider). |
| return length * 2; |
| } |
| return length; |
| } |
| |
| private boolean isTranscodeMtpEnabled() { |
| return SystemProperties.getBoolean("sys.fuse.transcode_mtp", false); |
| } |
| |
| private boolean isFileTranscodeSupported() { |
| // Check if the file supports transcoding by reading the |st_nlinks| struct stat |
| // field. This will be > 1 if the file supports transcoding. The FUSE daemon |
| // sets the field accordingly to enable the MTP stack workaround some Windows OS |
| // MTP client bug where they ignore the size returned as part of getting the MTP |
| // object, see MtpServer#doGetObject. |
| final Path path = getPath(); |
| try { |
| StructStat stat = Os.stat(path.toString()); |
| return stat.st_nlink > 1; |
| } catch (ErrnoException e) { |
| Log.w(TAG, "Failed to stat path: " + getPath() + ". Ignoring transcoding."); |
| return false; |
| } |
| } |
| |
| public Path getPath() { |
| return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName); |
| } |
| |
| public boolean isRoot() { |
| return mParent == null; |
| } |
| |
| public String getVolumeName() { |
| return mStorage.getVolumeName(); |
| } |
| |
| /** For MtpStorageManager only **/ |
| |
| private void setName(String name) { |
| mName = name; |
| } |
| |
| private void setId(int id) { |
| mId = id; |
| } |
| |
| private boolean isVisited() { |
| return mVisited; |
| } |
| |
| private void setParent(MtpObject parent) { |
| if (this.getStorageId() != parent.getStorageId()) { |
| mStorage = Preconditions.checkNotNull(parent.getStorage()); |
| } |
| mParent = parent; |
| } |
| |
| private MtpStorage getStorage() { |
| return mStorage; |
| } |
| |
| private void setDir(boolean dir) { |
| if (dir != mIsDir) { |
| mIsDir = dir; |
| mChildren = mIsDir ? new HashMap<>() : null; |
| } |
| } |
| |
| private void setVisited(boolean visited) { |
| mVisited = visited; |
| } |
| |
| private MtpObjectState getState() { |
| return mState; |
| } |
| |
| private void setState(MtpObjectState state) { |
| mState = state; |
| if (mState == MtpObjectState.NORMAL) |
| mOp = MtpOperation.NONE; |
| } |
| |
| private MtpOperation getOperation() { |
| return mOp; |
| } |
| |
| private void setOperation(MtpOperation op) { |
| mOp = op; |
| } |
| |
| private FileObserver getObserver() { |
| return mObserver; |
| } |
| |
| private void setObserver(FileObserver observer) { |
| mObserver = observer; |
| } |
| |
| private void addChild(MtpObject child) { |
| mChildren.put(child.getName(), child); |
| } |
| |
| private MtpObject getChild(String name) { |
| return mChildren.get(name); |
| } |
| |
| private Collection<MtpObject> getChildren() { |
| return mChildren.values(); |
| } |
| |
| private boolean exists() { |
| return getPath().toFile().exists(); |
| } |
| |
| private MtpObject copy(boolean recursive) { |
| MtpObject copy = new MtpObject(mName, mId, mStorage, mParent, mIsDir); |
| copy.mIsDir = mIsDir; |
| copy.mVisited = mVisited; |
| copy.mState = mState; |
| copy.mChildren = mIsDir ? new HashMap<>() : null; |
| if (recursive && mIsDir) { |
| for (MtpObject child : mChildren.values()) { |
| MtpObject childCopy = child.copy(true); |
| childCopy.setParent(copy); |
| copy.addChild(childCopy); |
| } |
| } |
| return copy; |
| } |
| } |
| |
| /** |
| * A class that processes generated filesystem events. |
| */ |
| public static abstract class MtpNotifier { |
| /** |
| * Called when an object is added. |
| */ |
| public abstract void sendObjectAdded(int id); |
| |
| /** |
| * Called when an object is deleted. |
| */ |
| public abstract void sendObjectRemoved(int id); |
| |
| /** |
| * Called when an object info is changed. |
| */ |
| public abstract void sendObjectInfoChanged(int id); |
| } |
| |
| private MtpNotifier mMtpNotifier; |
| |
| // A cache of MtpObjects. The objects in the cache are keyed by object id. |
| // The root object of each storage isn't in this map since they all have ObjectId 0. |
| // Instead, they can be found in mRoots keyed by storageId. |
| private HashMap<Integer, MtpObject> mObjects; |
| |
| // A cache of the root MtpObject for each storage, keyed by storage id. |
| private HashMap<Integer, MtpObject> mRoots; |
| |
| // Object and Storage ids are allocated incrementally and not to be reused. |
| private int mNextObjectId; |
| private int mNextStorageId; |
| |
| // Special subdirectories. When set, only return objects rooted in these directories, and do |
| // not allow them to be modified. |
| private Set<String> mSubdirectories; |
| |
| private volatile boolean mCheckConsistency; |
| private Thread mConsistencyThread; |
| |
| public MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories) { |
| mMtpNotifier = notifier; |
| mSubdirectories = subdirectories; |
| mObjects = new HashMap<>(); |
| mRoots = new HashMap<>(); |
| mNextObjectId = 1; |
| mNextStorageId = 1; |
| |
| mCheckConsistency = false; // Set to true to turn on automatic consistency checking |
| mConsistencyThread = new Thread(() -> { |
| while (mCheckConsistency) { |
| try { |
| Thread.sleep(15 * 1000); |
| } catch (InterruptedException e) { |
| return; |
| } |
| if (MtpStorageManager.this.checkConsistency()) { |
| Log.v(TAG, "Cache is consistent"); |
| } else { |
| Log.w(TAG, "Cache is not consistent"); |
| } |
| } |
| }); |
| if (mCheckConsistency) |
| mConsistencyThread.start(); |
| } |
| |
| /** |
| * Clean up resources used by the storage manager. |
| */ |
| public synchronized void close() { |
| for (MtpObject obj : mObjects.values()) { |
| if (obj.getObserver() != null) { |
| obj.getObserver().stopWatching(); |
| obj.setObserver(null); |
| } |
| } |
| for (MtpObject obj : mRoots.values()) { |
| if (obj.getObserver() != null) { |
| obj.getObserver().stopWatching(); |
| obj.setObserver(null); |
| } |
| } |
| |
| // Shut down the consistency checking thread |
| if (mCheckConsistency) { |
| mCheckConsistency = false; |
| mConsistencyThread.interrupt(); |
| try { |
| mConsistencyThread.join(); |
| } catch (InterruptedException e) { |
| // ignore |
| } |
| } |
| } |
| |
| /** |
| * Sets the special subdirectories, which are the subdirectories of root storage that queries |
| * are restricted to. Must be done before any root storages are accessed. |
| * @param subDirs Subdirectories to set, or null to reset. |
| */ |
| public synchronized void setSubdirectories(Set<String> subDirs) { |
| mSubdirectories = subDirs; |
| } |
| |
| /** |
| * Allocates an MTP storage id for the given volume and add it to current roots. |
| * @param volume Storage to add. |
| * @return the associated MtpStorage |
| */ |
| public synchronized MtpStorage addMtpStorage(StorageVolume volume, |
| Supplier<Boolean> isHostWindows) { |
| int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1; |
| MtpStorage storage = new MtpStorage(volume, storageId, isHostWindows); |
| MtpObject root = new MtpObject(storage.getPath(), storageId, storage, /* parent= */ null, |
| /* isDir= */ true); |
| mRoots.put(storageId, root); |
| return storage; |
| } |
| |
| /** |
| * Removes the given storage and all associated items from the cache. |
| * @param storage Storage to remove. |
| */ |
| public synchronized void removeMtpStorage(MtpStorage storage) { |
| removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true); |
| } |
| |
| /** |
| * Checks if the given object can be renamed, moved, or deleted. |
| * If there are special subdirectories, they cannot be modified. |
| * @param obj Object to check. |
| * @return Whether object can be modified. |
| */ |
| private synchronized boolean isSpecialSubDir(MtpObject obj) { |
| return obj.getParent().isRoot() && mSubdirectories != null |
| && !mSubdirectories.contains(obj.getName()); |
| } |
| |
| /** |
| * Get the object with the specified path. Visit any necessary directories on the way. |
| * @param path Full path of the object to find. |
| * @return The desired object, or null if it cannot be found. |
| */ |
| public synchronized MtpObject getByPath(String path) { |
| MtpObject obj = null; |
| for (MtpObject root : mRoots.values()) { |
| if (path.startsWith(root.getName())) { |
| obj = root; |
| path = path.substring(root.getName().length()); |
| } |
| } |
| for (String name : path.split("/")) { |
| if (obj == null || !obj.isDir()) |
| return null; |
| if ("".equals(name)) |
| continue; |
| if (!obj.isVisited()) |
| getChildren(obj); |
| obj = obj.getChild(name); |
| } |
| return obj; |
| } |
| |
| /** |
| * Get the object with specified id. |
| * @param id Id of object. must not be 0 or 0xFFFFFFFF |
| * @return Object, or null if error. |
| */ |
| public synchronized MtpObject getObject(int id) { |
| if (id == 0 || id == 0xFFFFFFFF) { |
| Log.w(TAG, "Can't get root storages with getObject()"); |
| return null; |
| } |
| if (!mObjects.containsKey(id)) { |
| Log.w(TAG, "Id " + id + " doesn't exist"); |
| return null; |
| } |
| return mObjects.get(id); |
| } |
| |
| /** |
| * Get the storage with specified id. |
| * @param id Storage id. |
| * @return Object that is the root of the storage, or null if error. |
| */ |
| public MtpObject getStorageRoot(int id) { |
| if (!mRoots.containsKey(id)) { |
| Log.w(TAG, "StorageId " + id + " doesn't exist"); |
| return null; |
| } |
| return mRoots.get(id); |
| } |
| |
| private int getNextObjectId() { |
| int ret = mNextObjectId; |
| // Treat the id as unsigned int |
| mNextObjectId = (int) ((long) mNextObjectId + 1); |
| return ret; |
| } |
| |
| private int getNextStorageId() { |
| return mNextStorageId++; |
| } |
| |
| /** |
| * Get all objects matching the given parent, format, and storage |
| * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root |
| * @param format format of returned objects. 0 for any format |
| * @param storageId storage id to look in. 0xFFFFFFFF for all storages |
| * @return A list of matched objects, or null if error |
| */ |
| public synchronized List<MtpObject> getObjects(int parent, int format, int storageId) { |
| boolean recursive = parent == 0; |
| ArrayList<MtpObject> objs = new ArrayList<>(); |
| boolean ret = true; |
| if (parent == 0xFFFFFFFF) |
| parent = 0; |
| if (storageId == 0xFFFFFFFF) { |
| // query all stores |
| if (parent == 0) { |
| // Get the objects of this format and parent in each store. |
| for (MtpObject root : mRoots.values()) { |
| ret &= getObjects(objs, root, format, recursive); |
| } |
| return ret ? objs : null; |
| } |
| } |
| MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent); |
| if (obj == null) |
| return null; |
| ret = getObjects(objs, obj, format, recursive); |
| return ret ? objs : null; |
| } |
| |
| private synchronized boolean getObjects(List<MtpObject> toAdd, MtpObject parent, int format, boolean rec) { |
| Collection<MtpObject> children = getChildren(parent); |
| if (children == null) |
| return false; |
| |
| for (MtpObject o : children) { |
| if (format == 0 || o.getFormat() == format) { |
| toAdd.add(o); |
| } |
| } |
| boolean ret = true; |
| if (rec) { |
| // Get all objects recursively. |
| for (MtpObject o : children) { |
| if (o.isDir()) |
| ret &= getObjects(toAdd, o, format, true); |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * Return the children of the given object. If the object hasn't been visited yet, add |
| * its children to the cache and start observing it. |
| * @param object the parent object |
| * @return The collection of child objects or null if error |
| */ |
| private synchronized Collection<MtpObject> getChildren(MtpObject object) { |
| if (object == null || !object.isDir()) { |
| Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId())); |
| return null; |
| } |
| if (!object.isVisited()) { |
| Path dir = object.getPath(); |
| /* |
| * If a file is added after the observer starts watching the directory, but before |
| * the contents are listed, it will generate an event that will get processed |
| * after this synchronized function returns. We handle this by ignoring object |
| * added events if an object at that path already exists. |
| */ |
| if (object.getObserver() != null) |
| Log.e(TAG, "Observer is not null!"); |
| object.setObserver(new MtpObjectObserver(object)); |
| object.getObserver().startWatching(); |
| try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { |
| for (Path file : stream) { |
| addObjectToCache(object, file.getFileName().toString(), |
| file.toFile().isDirectory()); |
| } |
| } catch (IOException | DirectoryIteratorException e) { |
| Log.e(TAG, e.toString()); |
| object.getObserver().stopWatching(); |
| object.setObserver(null); |
| return null; |
| } |
| object.setVisited(true); |
| } |
| return object.getChildren(); |
| } |
| |
| /** |
| * Create a new object from the given path and add it to the cache. |
| * @param parent The parent object |
| * @param newName Path of the new object |
| * @return the new object if success, else null |
| */ |
| private synchronized MtpObject addObjectToCache(MtpObject parent, String newName, |
| boolean isDir) { |
| if (!parent.isRoot() && getObject(parent.getId()) != parent) |
| // parent object has been removed |
| return null; |
| if (parent.getChild(newName) != null) { |
| // Object already exists |
| return null; |
| } |
| if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) { |
| // Not one of the restricted subdirectories. |
| return null; |
| } |
| |
| MtpObject obj = new MtpObject(newName, getNextObjectId(), parent.mStorage, parent, isDir); |
| mObjects.put(obj.getId(), obj); |
| parent.addChild(obj); |
| return obj; |
| } |
| |
| /** |
| * Remove the given path from the cache. |
| * @param removed The removed object |
| * @param removeGlobal Whether to remove the object from the global id map |
| * @param recursive Whether to also remove its children recursively. |
| * @return true if successfully removed |
| */ |
| private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal, |
| boolean recursive) { |
| boolean ret = removed.isRoot() |
| || removed.getParent().mChildren.remove(removed.getName(), removed); |
| if (!ret && sDebug) |
| Log.w(TAG, "Failed to remove from parent " + removed.getPath()); |
| if (removed.isRoot()) { |
| ret = mRoots.remove(removed.getId(), removed) && ret; |
| } else if (removeGlobal) { |
| ret = mObjects.remove(removed.getId(), removed) && ret; |
| } |
| if (!ret && sDebug) |
| Log.w(TAG, "Failed to remove from global cache " + removed.getPath()); |
| if (removed.getObserver() != null) { |
| removed.getObserver().stopWatching(); |
| removed.setObserver(null); |
| } |
| if (removed.isDir() && recursive) { |
| // Remove all descendants from cache recursively |
| Collection<MtpObject> children = new ArrayList<>(removed.getChildren()); |
| for (MtpObject child : children) { |
| ret = removeObjectFromCache(child, removeGlobal, true) && ret; |
| } |
| } |
| return ret; |
| } |
| |
| private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) { |
| MtpOperation op = MtpOperation.NONE; |
| MtpObject obj = parent.getChild(path); |
| if (obj != null) { |
| MtpObjectState state = obj.getState(); |
| op = obj.getOperation(); |
| if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED) |
| Log.d(TAG, "Inconsistent directory info! " + obj.getPath()); |
| obj.setDir(isDir); |
| switch (state) { |
| case FROZEN: |
| case FROZEN_REMOVED: |
| obj.setState(MtpObjectState.FROZEN_ADDED); |
| break; |
| case FROZEN_ONESHOT_ADD: |
| obj.setState(MtpObjectState.NORMAL); |
| break; |
| case NORMAL: |
| case FROZEN_ADDED: |
| // This can happen when handling listed object in a new directory. |
| return; |
| default: |
| Log.w(TAG, "Unexpected state in add " + path + " " + state); |
| } |
| if (sDebug) |
| Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op); |
| } else { |
| obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir); |
| if (obj != null) { |
| MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId()); |
| } else { |
| if (sDebug) |
| Log.w(TAG, "object " + path + " already exists"); |
| return; |
| } |
| } |
| if (isDir) { |
| // If this was added as part of a rename do not visit or send events. |
| if (op == MtpOperation.RENAME) |
| return; |
| |
| // If it was part of a copy operation, then only add observer if it was visited before. |
| if (op == MtpOperation.COPY && !obj.isVisited()) |
| return; |
| |
| if (obj.getObserver() != null) { |
| Log.e(TAG, "Observer is not null!"); |
| return; |
| } |
| obj.setObserver(new MtpObjectObserver(obj)); |
| obj.getObserver().startWatching(); |
| obj.setVisited(true); |
| |
| // It's possible that objects were added to a watched directory before the watch can be |
| // created, so manually handle those. |
| try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) { |
| for (Path file : stream) { |
| if (sDebug) |
| Log.i(TAG, "Manually handling event for " + file.getFileName().toString()); |
| handleAddedObject(obj, file.getFileName().toString(), |
| file.toFile().isDirectory()); |
| } |
| } catch (IOException | DirectoryIteratorException e) { |
| Log.e(TAG, e.toString()); |
| obj.getObserver().stopWatching(); |
| obj.setObserver(null); |
| } |
| } |
| } |
| |
| private synchronized void handleRemovedObject(MtpObject obj) { |
| MtpObjectState state = obj.getState(); |
| MtpOperation op = obj.getOperation(); |
| switch (state) { |
| case FROZEN_ADDED: |
| obj.setState(MtpObjectState.FROZEN_REMOVED); |
| break; |
| case FROZEN_ONESHOT_DEL: |
| removeObjectFromCache(obj, op != MtpOperation.RENAME, false); |
| break; |
| case FROZEN: |
| obj.setState(MtpObjectState.FROZEN_REMOVED); |
| break; |
| case NORMAL: |
| if (MtpStorageManager.this.removeObjectFromCache(obj, true, true)) |
| MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId()); |
| break; |
| default: |
| // This shouldn't happen; states correspond to objects that don't exist |
| Log.e(TAG, "Got unexpected object remove for " + obj.getName()); |
| } |
| if (sDebug) |
| Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op); |
| } |
| |
| private synchronized void handleChangedObject(MtpObject parent, String path) { |
| MtpOperation op = MtpOperation.NONE; |
| MtpObject obj = parent.getChild(path); |
| if (obj != null) { |
| // Only handle files for size change notification event |
| if ((!obj.isDir()) && (obj.getSize() > 0)) |
| { |
| MtpObjectState state = obj.getState(); |
| op = obj.getOperation(); |
| MtpStorageManager.this.mMtpNotifier.sendObjectInfoChanged(obj.getId()); |
| if (sDebug) |
| Log.d(TAG, "sendObjectInfoChanged: id=" + obj.getId() + ",size=" + obj.getSize()); |
| } |
| } else { |
| if (sDebug) |
| Log.w(TAG, "object " + path + " null"); |
| } |
| } |
| |
| /** |
| * Block the caller until all events currently in the event queue have been |
| * read and processed. Used for testing purposes. |
| */ |
| public void flushEvents() { |
| try { |
| // TODO make this smarter |
| Thread.sleep(500); |
| } catch (InterruptedException e) { |
| |
| } |
| } |
| |
| /** |
| * Dumps a representation of the cache to log. |
| */ |
| public synchronized void dump() { |
| for (int key : mObjects.keySet()) { |
| MtpObject obj = mObjects.get(key); |
| Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null") |
| + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj") |
| + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState()); |
| } |
| } |
| |
| /** |
| * Checks consistency of the cache. This checks whether all objects have correct links |
| * to their parent, and whether directories are missing or have extraneous objects. |
| * @return true iff cache is consistent |
| */ |
| public synchronized boolean checkConsistency() { |
| List<MtpObject> objs = new ArrayList<>(); |
| objs.addAll(mRoots.values()); |
| objs.addAll(mObjects.values()); |
| boolean ret = true; |
| for (MtpObject obj : objs) { |
| if (!obj.exists()) { |
| Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId()); |
| ret = false; |
| } |
| if (obj.getState() != MtpObjectState.NORMAL) { |
| Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState()); |
| ret = false; |
| } |
| if (obj.getOperation() != MtpOperation.NONE) { |
| Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation()); |
| ret = false; |
| } |
| if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) { |
| Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly"); |
| ret = false; |
| } |
| if (obj.getParent() != null) { |
| if (obj.getParent().isRoot() && obj.getParent() |
| != mRoots.get(obj.getParent().getId())) { |
| Log.w(TAG, "Root parent is not in root mapping " + obj.getPath()); |
| ret = false; |
| } |
| if (!obj.getParent().isRoot() && obj.getParent() |
| != mObjects.get(obj.getParent().getId())) { |
| Log.w(TAG, "Parent is not in object mapping " + obj.getPath()); |
| ret = false; |
| } |
| if (obj.getParent().getChild(obj.getName()) != obj) { |
| Log.w(TAG, "Child does not exist in parent " + obj.getPath()); |
| ret = false; |
| } |
| } |
| if (obj.isDir()) { |
| if (obj.isVisited() == (obj.getObserver() == null)) { |
| Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ") |
| + " visited but observer is " + obj.getObserver()); |
| ret = false; |
| } |
| if (!obj.isVisited() && obj.getChildren().size() > 0) { |
| Log.w(TAG, obj.getPath() + " is not visited but has children"); |
| ret = false; |
| } |
| try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) { |
| Set<String> files = new HashSet<>(); |
| for (Path file : stream) { |
| if (obj.isVisited() && |
| obj.getChild(file.getFileName().toString()) == null && |
| (mSubdirectories == null || !obj.isRoot() || |
| mSubdirectories.contains(file.getFileName().toString()))) { |
| Log.w(TAG, "File exists in fs but not in children " + file); |
| ret = false; |
| } |
| files.add(file.toString()); |
| } |
| for (MtpObject child : obj.getChildren()) { |
| if (!files.contains(child.getPath().toString())) { |
| Log.w(TAG, "File in children doesn't exist in fs " + child.getPath()); |
| ret = false; |
| } |
| if (child != mObjects.get(child.getId())) { |
| Log.w(TAG, "Child is not in object map " + child.getPath()); |
| ret = false; |
| } |
| } |
| } catch (IOException | DirectoryIteratorException e) { |
| Log.w(TAG, e.toString()); |
| ret = false; |
| } |
| } |
| } |
| return ret; |
| } |
| |
| /** |
| * Informs MtpStorageManager that an object with the given path is about to be added. |
| * @param parent The parent object of the object to be added. |
| * @param name Filename of object to add. |
| * @return Object id of the added object, or -1 if it cannot be added. |
| */ |
| public synchronized int beginSendObject(MtpObject parent, String name, int format) { |
| if (sDebug) |
| Log.v(TAG, "beginSendObject " + name); |
| if (!parent.isDir()) |
| return -1; |
| if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name)) |
| return -1; |
| getChildren(parent); // Ensure parent is visited |
| MtpObject obj = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION); |
| if (obj == null) |
| return -1; |
| obj.setState(MtpObjectState.FROZEN); |
| obj.setOperation(MtpOperation.ADD); |
| return obj.getId(); |
| } |
| |
| /** |
| * Clean up the object state after a sendObject operation. |
| * @param obj The object, returned from beginAddObject(). |
| * @param succeeded Whether the file was successfully created. |
| * @return Whether cache state was successfully cleaned up. |
| */ |
| public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) { |
| if (sDebug) |
| Log.v(TAG, "endSendObject " + succeeded); |
| return generalEndAddObject(obj, succeeded, true); |
| } |
| |
| /** |
| * Informs MtpStorageManager that the given object is about to be renamed. |
| * If this returns true, it must be followed with an endRenameObject() |
| * @param obj Object to be renamed. |
| * @param newName New name of the object. |
| * @return Whether renaming is allowed. |
| */ |
| public synchronized boolean beginRenameObject(MtpObject obj, String newName) { |
| if (sDebug) |
| Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName); |
| if (obj.isRoot()) |
| return false; |
| if (isSpecialSubDir(obj)) |
| return false; |
| if (obj.getParent().getChild(newName) != null) |
| // Object already exists in parent with that name. |
| return false; |
| |
| MtpObject oldObj = obj.copy(false); |
| obj.setName(newName); |
| obj.getParent().addChild(obj); |
| oldObj.getParent().addChild(oldObj); |
| return generalBeginRenameObject(oldObj, obj); |
| } |
| |
| /** |
| * Cleans up cache state after a rename operation and sends any events that were missed. |
| * @param obj The object being renamed, the same one that was passed in beginRenameObject(). |
| * @param oldName The previous name of the object. |
| * @param success Whether the rename operation succeeded. |
| * @return Whether state was successfully cleaned up. |
| */ |
| public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) { |
| if (sDebug) |
| Log.v(TAG, "endRenameObject " + success); |
| MtpObject parent = obj.getParent(); |
| MtpObject oldObj = parent.getChild(oldName); |
| if (!success) { |
| // If the rename failed, we want oldObj to be the original and obj to be the stand-in. |
| // Switch the objects, except for their name and state. |
| MtpObject temp = oldObj; |
| MtpObjectState oldState = oldObj.getState(); |
| temp.setName(obj.getName()); |
| temp.setState(obj.getState()); |
| oldObj = obj; |
| oldObj.setName(oldName); |
| oldObj.setState(oldState); |
| obj = temp; |
| parent.addChild(obj); |
| parent.addChild(oldObj); |
| } |
| return generalEndRenameObject(oldObj, obj, success); |
| } |
| |
| /** |
| * Informs MtpStorageManager that the given object is about to be deleted by the initiator, |
| * so don't send an event. |
| * @param obj Object to be deleted. |
| * @return Whether cache deletion is allowed. |
| */ |
| public synchronized boolean beginRemoveObject(MtpObject obj) { |
| if (sDebug) |
| Log.v(TAG, "beginRemoveObject " + obj.getName()); |
| return !obj.isRoot() && !isSpecialSubDir(obj) |
| && generalBeginRemoveObject(obj, MtpOperation.DELETE); |
| } |
| |
| /** |
| * Clean up cache state after a delete operation and send any events that were missed. |
| * @param obj Object to be deleted, same one passed in beginRemoveObject(). |
| * @param success Whether operation was completed successfully. |
| * @return Whether cache state is correct. |
| */ |
| public synchronized boolean endRemoveObject(MtpObject obj, boolean success) { |
| if (sDebug) |
| Log.v(TAG, "endRemoveObject " + success); |
| boolean ret = true; |
| if (obj.isDir()) { |
| for (MtpObject child : new ArrayList<>(obj.getChildren())) |
| if (child.getOperation() == MtpOperation.DELETE) |
| ret = endRemoveObject(child, success) && ret; |
| } |
| return generalEndRemoveObject(obj, success, true) && ret; |
| } |
| |
| /** |
| * Informs MtpStorageManager that the given object is about to be moved to a new parent. |
| * @param obj Object to be moved. |
| * @param newParent The new parent object. |
| * @return Whether the move is allowed. |
| */ |
| public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) { |
| if (sDebug) |
| Log.v(TAG, "beginMoveObject " + newParent.getPath()); |
| if (obj.isRoot()) |
| return false; |
| if (isSpecialSubDir(obj)) |
| return false; |
| getChildren(newParent); // Ensure parent is visited |
| if (newParent.getChild(obj.getName()) != null) |
| // Object already exists in parent with that name. |
| return false; |
| if (obj.getStorageId() != newParent.getStorageId()) { |
| /* |
| * The move is occurring across storages. The observers will not remain functional |
| * after the move, and the move will not be atomic. We have to copy the file tree |
| * to the destination and recreate the observers once copy is complete. |
| */ |
| MtpObject newObj = obj.copy(true); |
| newObj.setParent(newParent); |
| newParent.addChild(newObj); |
| return generalBeginRemoveObject(obj, MtpOperation.RENAME) |
| && generalBeginCopyObject(newObj, false); |
| } |
| // Move obj to new parent, create a fake object in the old parent. |
| MtpObject oldObj = obj.copy(false); |
| obj.setParent(newParent); |
| oldObj.getParent().addChild(oldObj); |
| obj.getParent().addChild(obj); |
| return generalBeginRenameObject(oldObj, obj); |
| } |
| |
| /** |
| * Clean up cache state after a move operation and send any events that were missed. |
| * @param oldParent The old parent object. |
| * @param newParent The new parent object. |
| * @param name The name of the object being moved. |
| * @param success Whether operation was completed successfully. |
| * @return Whether cache state is correct. |
| */ |
| public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name, |
| boolean success) { |
| if (sDebug) |
| Log.v(TAG, "endMoveObject " + success); |
| MtpObject oldObj = oldParent.getChild(name); |
| MtpObject newObj = newParent.getChild(name); |
| if (oldObj == null || newObj == null) |
| return false; |
| if (oldParent.getStorageId() != newObj.getStorageId()) { |
| boolean ret = endRemoveObject(oldObj, success); |
| return generalEndCopyObject(newObj, success, true) && ret; |
| } |
| if (!success) { |
| // If the rename failed, we want oldObj to be the original and obj to be the stand-in. |
| // Switch the objects, except for their parent and state. |
| MtpObject temp = oldObj; |
| MtpObjectState oldState = oldObj.getState(); |
| temp.setParent(newObj.getParent()); |
| temp.setState(newObj.getState()); |
| oldObj = newObj; |
| oldObj.setParent(oldParent); |
| oldObj.setState(oldState); |
| newObj = temp; |
| newObj.getParent().addChild(newObj); |
| oldParent.addChild(oldObj); |
| } |
| return generalEndRenameObject(oldObj, newObj, success); |
| } |
| |
| /** |
| * Informs MtpStorageManager that the given object is about to be copied recursively. |
| * @param object Object to be copied |
| * @param newParent New parent for the object. |
| * @return The object id for the new copy, or -1 if error. |
| */ |
| public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) { |
| if (sDebug) |
| Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath()); |
| String name = object.getName(); |
| if (!newParent.isDir()) |
| return -1; |
| if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name)) |
| return -1; |
| getChildren(newParent); // Ensure parent is visited |
| if (newParent.getChild(name) != null) |
| return -1; |
| MtpObject newObj = object.copy(object.isDir()); |
| newParent.addChild(newObj); |
| newObj.setParent(newParent); |
| if (!generalBeginCopyObject(newObj, true)) |
| return -1; |
| return newObj.getId(); |
| } |
| |
| /** |
| * Cleans up cache state after a copy operation. |
| * @param object Object that was copied. |
| * @param success Whether the operation was successful. |
| * @return Whether cache state is consistent. |
| */ |
| public synchronized boolean endCopyObject(MtpObject object, boolean success) { |
| if (sDebug) |
| Log.v(TAG, "endCopyObject " + object.getName() + " " + success); |
| return generalEndCopyObject(object, success, false); |
| } |
| |
| private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded, |
| boolean removeGlobal) { |
| switch (obj.getState()) { |
| case FROZEN: |
| // Object was never created. |
| if (succeeded) { |
| // The operation was successful so the event must still be in the queue. |
| obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD); |
| } else { |
| // The operation failed and never created the file. |
| if (!removeObjectFromCache(obj, removeGlobal, false)) { |
| return false; |
| } |
| } |
| break; |
| case FROZEN_ADDED: |
| obj.setState(MtpObjectState.NORMAL); |
| if (!succeeded) { |
| MtpObject parent = obj.getParent(); |
| // The operation failed but some other process created the file. Send an event. |
| if (!removeObjectFromCache(obj, removeGlobal, false)) |
| return false; |
| handleAddedObject(parent, obj.getName(), obj.isDir()); |
| } |
| // else: The operation successfully created the object. |
| break; |
| case FROZEN_REMOVED: |
| if (!removeObjectFromCache(obj, removeGlobal, false)) |
| return false; |
| if (succeeded) { |
| // Some other process deleted the object. Send an event. |
| mMtpNotifier.sendObjectRemoved(obj.getId()); |
| } |
| // else: Mtp deleted the object as part of cleanup. Don't send an event. |
| break; |
| default: |
| return false; |
| } |
| return true; |
| } |
| |
| private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success, |
| boolean removeGlobal) { |
| switch (obj.getState()) { |
| case FROZEN: |
| if (success) { |
| // Object was deleted successfully, and event is still in the queue. |
| obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL); |
| } else { |
| // Object was not deleted. |
| obj.setState(MtpObjectState.NORMAL); |
| } |
| break; |
| case FROZEN_ADDED: |
| // Object was deleted, and then readded. |
| obj.setState(MtpObjectState.NORMAL); |
| if (success) { |
| // Some other process readded the object. |
| MtpObject parent = obj.getParent(); |
| if (!removeObjectFromCache(obj, removeGlobal, false)) |
| return false; |
| handleAddedObject(parent, obj.getName(), obj.isDir()); |
| } |
| // else : Object still exists after failure. |
| break; |
| case FROZEN_REMOVED: |
| if (!removeObjectFromCache(obj, removeGlobal, false)) |
| return false; |
| if (!success) { |
| // Some other process deleted the object. |
| mMtpNotifier.sendObjectRemoved(obj.getId()); |
| } |
| // else : This process deleted the object as part of the operation. |
| break; |
| default: |
| return false; |
| } |
| return true; |
| } |
| |
| private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) { |
| fromObj.setState(MtpObjectState.FROZEN); |
| toObj.setState(MtpObjectState.FROZEN); |
| fromObj.setOperation(MtpOperation.RENAME); |
| toObj.setOperation(MtpOperation.RENAME); |
| return true; |
| } |
| |
| private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj, |
| boolean success) { |
| boolean ret = generalEndRemoveObject(fromObj, success, !success); |
| return generalEndAddObject(toObj, success, success) && ret; |
| } |
| |
| private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) { |
| obj.setState(MtpObjectState.FROZEN); |
| obj.setOperation(op); |
| if (obj.isDir()) { |
| for (MtpObject child : obj.getChildren()) |
| generalBeginRemoveObject(child, op); |
| } |
| return true; |
| } |
| |
| private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) { |
| obj.setState(MtpObjectState.FROZEN); |
| obj.setOperation(MtpOperation.COPY); |
| if (newId) { |
| obj.setId(getNextObjectId()); |
| mObjects.put(obj.getId(), obj); |
| } |
| if (obj.isDir()) |
| for (MtpObject child : obj.getChildren()) |
| if (!generalBeginCopyObject(child, newId)) |
| return false; |
| return true; |
| } |
| |
| private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) { |
| if (success && addGlobal) |
| mObjects.put(obj.getId(), obj); |
| boolean ret = true; |
| if (obj.isDir()) { |
| for (MtpObject child : new ArrayList<>(obj.getChildren())) { |
| if (child.getOperation() == MtpOperation.COPY) |
| ret = generalEndCopyObject(child, success, addGlobal) && ret; |
| } |
| } |
| ret = generalEndAddObject(obj, success, success || !addGlobal) && ret; |
| return ret; |
| } |
| } |