Add MtpStorageManager for monitoring filesystem events

MtpStorageManager keeps track of file information and
send notifications for new files. MtpDatabase now uses
this instead of MediaProvider for getting object information,
although some operations are still reflected into MP.

Since MtpStorageManager handles storage ids, remove
that field from StorageVolume and VolumeInfo.

Clean up a lot of the jni code for MtpDatabase.

Bug: 63143623
Test: Test every MtpOperation in a variety of situations on Linux and
Windows. Also use the shell to manipulate files. Verify that the cache
is consistent throughout, and the operations behave as expected. Verify
files created by the shell appear.
Test: adb shell am instrument -w android.mtp /android.support.test.runner.AndroidJUnitRunner
Change-Id: Id4ea810047b0c323399cd833047733e5daafb30a
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index 1fc0b82..070b8c1 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -19,7 +19,6 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
-import android.net.TrafficStats;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.Parcel;
@@ -78,13 +77,11 @@
 public final class StorageVolume implements Parcelable {
 
     private final String mId;
-    private final int mStorageId;
     private final File mPath;
     private final String mDescription;
     private final boolean mPrimary;
     private final boolean mRemovable;
     private final boolean mEmulated;
-    private final long mMtpReserveSize;
     private final boolean mAllowMassStorage;
     private final long mMaxFileSize;
     private final UserHandle mOwner;
@@ -121,17 +118,15 @@
     public static final int STORAGE_ID_PRIMARY = 0x00010001;
 
     /** {@hide} */
-    public StorageVolume(String id, int storageId, File path, String description, boolean primary,
-            boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage,
+    public StorageVolume(String id, File path, String description, boolean primary,
+            boolean removable, boolean emulated, boolean allowMassStorage,
             long maxFileSize, UserHandle owner, String fsUuid, String state) {
         mId = Preconditions.checkNotNull(id);
-        mStorageId = storageId;
         mPath = Preconditions.checkNotNull(path);
         mDescription = Preconditions.checkNotNull(description);
         mPrimary = primary;
         mRemovable = removable;
         mEmulated = emulated;
-        mMtpReserveSize = mtpReserveSize;
         mAllowMassStorage = allowMassStorage;
         mMaxFileSize = maxFileSize;
         mOwner = Preconditions.checkNotNull(owner);
@@ -141,13 +136,11 @@
 
     private StorageVolume(Parcel in) {
         mId = in.readString();
-        mStorageId = in.readInt();
         mPath = new File(in.readString());
         mDescription = in.readString();
         mPrimary = in.readInt() != 0;
         mRemovable = in.readInt() != 0;
         mEmulated = in.readInt() != 0;
-        mMtpReserveSize = in.readLong();
         mAllowMassStorage = in.readInt() != 0;
         mMaxFileSize = in.readLong();
         mOwner = in.readParcelable(null);
@@ -211,34 +204,6 @@
     }
 
     /**
-     * Returns the MTP storage ID for the volume.
-     * this is also used for the storage_id column in the media provider.
-     *
-     * @return MTP storage ID
-     * @hide
-     */
-    public int getStorageId() {
-        return mStorageId;
-    }
-
-    /**
-     * Number of megabytes of space to leave unallocated by MTP.
-     * MTP will subtract this value from the free space it reports back
-     * to the host via GetStorageInfo, and will not allow new files to
-     * be added via MTP if there is less than this amount left free in the storage.
-     * If MTP has dedicated storage this value should be zero, but if MTP is
-     * sharing storage with the rest of the system, set this to a positive value
-     * to ensure that MTP activity does not result in the storage being
-     * too close to full.
-     *
-     * @return MTP reserve space
-     * @hide
-     */
-    public int getMtpReserveSpace() {
-        return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES);
-    }
-
-    /**
      * Returns true if this volume can be shared via USB mass storage.
      *
      * @return whether mass storage is allowed
@@ -385,13 +350,11 @@
         pw.println("StorageVolume:");
         pw.increaseIndent();
         pw.printPair("mId", mId);
-        pw.printPair("mStorageId", mStorageId);
         pw.printPair("mPath", mPath);
         pw.printPair("mDescription", mDescription);
         pw.printPair("mPrimary", mPrimary);
         pw.printPair("mRemovable", mRemovable);
         pw.printPair("mEmulated", mEmulated);
-        pw.printPair("mMtpReserveSize", mMtpReserveSize);
         pw.printPair("mAllowMassStorage", mAllowMassStorage);
         pw.printPair("mMaxFileSize", mMaxFileSize);
         pw.printPair("mOwner", mOwner);
@@ -420,13 +383,11 @@
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeString(mId);
-        parcel.writeInt(mStorageId);
         parcel.writeString(mPath.toString());
         parcel.writeString(mDescription);
         parcel.writeInt(mPrimary ? 1 : 0);
         parcel.writeInt(mRemovable ? 1 : 0);
         parcel.writeInt(mEmulated ? 1 : 0);
-        parcel.writeLong(mMtpReserveSize);
         parcel.writeInt(mAllowMassStorage ? 1 : 0);
         parcel.writeLong(mMaxFileSize);
         parcel.writeParcelable(mOwner, flags);
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index 76f79f1..d3877ca 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -343,9 +343,7 @@
 
         String description = null;
         String derivedFsUuid = fsUuid;
-        long mtpReserveSize = 0;
         long maxFileSize = 0;
-        int mtpStorageId = StorageVolume.STORAGE_ID_INVALID;
 
         if (type == TYPE_EMULATED) {
             emulated = true;
@@ -356,12 +354,6 @@
                 derivedFsUuid = privateVol.fsUuid;
             }
 
-            if (isPrimary()) {
-                mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
-            }
-
-            mtpReserveSize = storage.getStorageLowBytes(userPath);
-
             if (ID_EMULATED_INTERNAL.equals(id)) {
                 removable = false;
             } else {
@@ -374,14 +366,6 @@
 
             description = storage.getBestVolumeDescription(this);
 
-            if (isPrimary()) {
-                mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
-            } else {
-                // Since MediaProvider currently persists this value, we need a
-                // value that is stable over time.
-                mtpStorageId = buildStableMtpStorageId(fsUuid);
-            }
-
             if ("vfat".equals(fsType)) {
                 maxFileSize = 4294967295L;
             }
@@ -394,8 +378,8 @@
             description = context.getString(android.R.string.unknownName);
         }
 
-        return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable,
-                emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId),
+        return new StorageVolume(id, userPath, description, isPrimary(), removable,
+                emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
                 derivedFsUuid, envState);
     }
 
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 32d68cd..d9808a3 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -63,15 +63,6 @@
 
     private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
 
-   /**
-     * Broadcast Action:  A broadcast to indicate the end of an MTP session with the host.
-     * This broadcast is only sent if MTP activity has modified the media database during the
-     * most recent MTP session.
-     *
-     * @hide
-     */
-    public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END";
-
     /**
      * The method name used by the media scanner and mtp to tell the media provider to
      * rescan and reclassify that have become unhidden because of renaming folders or
diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java
index ba29d2d..a647dcc 100755
--- a/media/java/android/mtp/MtpDatabase.java
+++ b/media/java/android/mtp/MtpDatabase.java
@@ -30,6 +30,7 @@
 import android.os.BatteryManager;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Files;
@@ -40,21 +41,31 @@
 
 import dalvik.system.CloseGuard;
 
+import com.google.android.collect.Sets;
+
 import java.io.File;
-import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 /**
+ * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
+ * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
+ * operations are also reflected in MediaProvider if possible.
+ * operations
  * {@hide}
  */
 public class MtpDatabase implements AutoCloseable {
-    private static final String TAG = "MtpDatabase";
+    private static final String TAG = MtpDatabase.class.getSimpleName();
 
-    private final Context mUserContext;
     private final Context mContext;
-    private final String mPackageName;
     private final ContentProviderClient mMediaProvider;
     private final String mVolumeName;
     private final Uri mObjectsUri;
@@ -63,527 +74,36 @@
     private final AtomicBoolean mClosed = new AtomicBoolean();
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
-    // path to primary storage
-    private final String mMediaStoragePath;
-    // if not null, restrict all queries to these subdirectories
-    private final String[] mSubDirectories;
-    // where clause for restricting queries to files in mSubDirectories
-    private String mSubDirectoriesWhere;
-    // where arguments for restricting queries to files in mSubDirectories
-    private String[] mSubDirectoriesWhereArgs;
-
-    private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>();
+    private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>();
 
     // cached property groups for single properties
-    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty
-            = new HashMap<Integer, MtpPropertyGroup>();
+    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty = new HashMap<>();
 
     // cached property groups for all properties for a given format
-    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat
-            = new HashMap<Integer, MtpPropertyGroup>();
-
-    // true if the database has been modified in the current MTP session
-    private boolean mDatabaseModified;
+    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat = new HashMap<>();
 
     // SharedPreferences for writable MTP device properties
     private SharedPreferences mDeviceProperties;
-    private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1;
 
-    private static final String[] ID_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-    };
-    private static final String[] PATH_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.DATA, // 1
-    };
-    private static final String[] FORMAT_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.FORMAT, // 1
-    };
-    private static final String[] PATH_FORMAT_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.DATA, // 1
-            Files.FileColumns.FORMAT, // 2
-    };
-    private static final String[] OBJECT_INFO_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.STORAGE_ID, // 1
-            Files.FileColumns.FORMAT, // 2
-            Files.FileColumns.PARENT, // 3
-            Files.FileColumns.DATA, // 4
-            Files.FileColumns.DATE_ADDED, // 5
-            Files.FileColumns.DATE_MODIFIED, // 6
-    };
-    private static final String ID_WHERE = Files.FileColumns._ID + "=?";
-    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
-
-    private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?";
-    private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
-    private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
-    private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND "
-                                            + Files.FileColumns.FORMAT + "=?";
-    private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND "
-                                            + Files.FileColumns.PARENT + "=?";
-    private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND "
-                                            + Files.FileColumns.PARENT + "=?";
-    private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND "
-                                            + Files.FileColumns.PARENT + "=?";
-
-    private MtpServer mServer;
-
-    // read from native code
+    // Cached device properties
     private int mBatteryLevel;
     private int mBatteryScale;
-
     private int mDeviceType;
 
+    private MtpServer mServer;
+    private MtpStorageManager mManager;
+
+    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
+    private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID};
+    private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA};
+    private static final String NO_MEDIA = ".nomedia";
+
     static {
         System.loadLibrary("media_jni");
     }
 
-    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
-          @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
-                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
-                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
-                if (newLevel != mBatteryLevel) {
-                    mBatteryLevel = newLevel;
-                    if (mServer != null) {
-                        // send device property changed event
-                        mServer.sendDevicePropertyChanged(
-                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
-                    }
-                }
-            }
-        }
-    };
-
-    public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath,
-            String[] subDirectories) {
-        native_setup();
-
-        mContext = context;
-        mUserContext = userContext;
-        mPackageName = context.getPackageName();
-        mMediaProvider = userContext.getContentResolver()
-                .acquireContentProviderClient(MediaStore.AUTHORITY);
-        mVolumeName = volumeName;
-        mMediaStoragePath = storagePath;
-        mObjectsUri = Files.getMtpObjectsUri(volumeName);
-        mMediaScanner = new MediaScanner(context, mVolumeName);
-
-        mSubDirectories = subDirectories;
-        if (subDirectories != null) {
-            // Compute "where" string for restricting queries to subdirectories
-            StringBuilder builder = new StringBuilder();
-            builder.append("(");
-            int count = subDirectories.length;
-            for (int i = 0; i < count; i++) {
-                builder.append(Files.FileColumns.DATA + "=? OR "
-                        + Files.FileColumns.DATA + " LIKE ?");
-                if (i != count - 1) {
-                    builder.append(" OR ");
-                }
-            }
-            builder.append(")");
-            mSubDirectoriesWhere = builder.toString();
-
-            // Compute "where" arguments for restricting queries to subdirectories
-            mSubDirectoriesWhereArgs = new String[count * 2];
-            for (int i = 0, j = 0; i < count; i++) {
-                String path = subDirectories[i];
-                mSubDirectoriesWhereArgs[j++] = path;
-                mSubDirectoriesWhereArgs[j++] = path + "/%";
-            }
-        }
-
-        initDeviceProperties(context);
-        mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
-
-        mCloseGuard.open("close");
-    }
-
-    public void setServer(MtpServer server) {
-        mServer = server;
-
-        // always unregister before registering
-        try {
-            mContext.unregisterReceiver(mBatteryReceiver);
-        } catch (IllegalArgumentException e) {
-            // wasn't previously registered, ignore
-        }
-
-        // register for battery notifications when we are connected
-        if (server != null) {
-            mContext.registerReceiver(mBatteryReceiver,
-                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
-        }
-    }
-
-    @Override
-    public void close() {
-        mCloseGuard.close();
-        if (mClosed.compareAndSet(false, true)) {
-            mMediaScanner.close();
-            mMediaProvider.close();
-            native_finalize();
-        }
-    }
-
-    @Override
-    protected void finalize() throws Throwable {
-        try {
-            if (mCloseGuard != null) {
-                mCloseGuard.warnIfOpen();
-            }
-
-            close();
-        } finally {
-            super.finalize();
-        }
-    }
-
-    public void addStorage(MtpStorage storage) {
-        mStorageMap.put(storage.getPath(), storage);
-    }
-
-    public void removeStorage(MtpStorage storage) {
-        mStorageMap.remove(storage.getPath());
-    }
-
-    private void initDeviceProperties(Context context) {
-        final String devicePropertiesName = "device-properties";
-        mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE);
-        File databaseFile = context.getDatabasePath(devicePropertiesName);
-
-        if (databaseFile.exists()) {
-            // for backward compatibility - read device properties from sqlite database
-            // and migrate them to shared prefs
-            SQLiteDatabase db = null;
-            Cursor c = null;
-            try {
-                db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
-                if (db != null) {
-                    c = db.query("properties", new String[] { "_id", "code", "value" },
-                            null, null, null, null, null);
-                    if (c != null) {
-                        SharedPreferences.Editor e = mDeviceProperties.edit();
-                        while (c.moveToNext()) {
-                            String name = c.getString(1);
-                            String value = c.getString(2);
-                            e.putString(name, value);
-                        }
-                        e.commit();
-                    }
-                }
-            } catch (Exception e) {
-                Log.e(TAG, "failed to migrate device properties", e);
-            } finally {
-                if (c != null) c.close();
-                if (db != null) db.close();
-            }
-            context.deleteDatabase(devicePropertiesName);
-        }
-    }
-
-    // check to see if the path is contained in one of our storage subdirectories
-    // returns true if we have no special subdirectories
-    private boolean inStorageSubDirectory(String path) {
-        if (mSubDirectories == null) return true;
-        if (path == null) return false;
-
-        boolean allowed = false;
-        int pathLength = path.length();
-        for (int i = 0; i < mSubDirectories.length && !allowed; i++) {
-            String subdir = mSubDirectories[i];
-            int subdirLength = subdir.length();
-            if (subdirLength < pathLength &&
-                    path.charAt(subdirLength) == '/' &&
-                    path.startsWith(subdir)) {
-                allowed = true;
-            }
-        }
-        return allowed;
-    }
-
-    // check to see if the path matches one of our storage subdirectories
-    // returns true if we have no special subdirectories
-    private boolean isStorageSubDirectory(String path) {
-    if (mSubDirectories == null) return false;
-        for (int i = 0; i < mSubDirectories.length; i++) {
-            if (path.equals(mSubDirectories[i])) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    // returns true if the path is in the storage root
-    private boolean inStorageRoot(String path) {
-        try {
-            File f = new File(path);
-            String canonical = f.getCanonicalPath();
-            for (String root: mStorageMap.keySet()) {
-                if (canonical.startsWith(root)) {
-                    return true;
-                }
-            }
-        } catch (IOException e) {
-            // ignore
-        }
-        return false;
-    }
-
-    private int beginSendObject(String path, int format, int parent,
-                         int storageId, long size, long modified) {
-        // if the path is outside of the storage root, do not allow access
-        if (!inStorageRoot(path)) {
-            Log.e(TAG, "attempt to put file outside of storage area: " + path);
-            return -1;
-        }
-        // if mSubDirectories is not null, do not allow copying files to any other locations
-        if (!inStorageSubDirectory(path)) return -1;
-
-        // make sure the object does not exist
-        if (path != null) {
-            Cursor c = null;
-            try {
-                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
-                        new String[] { path }, null, null);
-                if (c != null && c.getCount() > 0) {
-                    Log.w(TAG, "file already exists in beginSendObject: " + path);
-                    return -1;
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException in beginSendObject", e);
-            } finally {
-                if (c != null) {
-                    c.close();
-                }
-            }
-        }
-
-        mDatabaseModified = true;
-        ContentValues values = new ContentValues();
-        values.put(Files.FileColumns.DATA, path);
-        values.put(Files.FileColumns.FORMAT, format);
-        values.put(Files.FileColumns.PARENT, parent);
-        values.put(Files.FileColumns.STORAGE_ID, storageId);
-        values.put(Files.FileColumns.SIZE, size);
-        values.put(Files.FileColumns.DATE_MODIFIED, modified);
-
-        try {
-            Uri uri = mMediaProvider.insert(mObjectsUri, values);
-            if (uri != null) {
-                return Integer.parseInt(uri.getPathSegments().get(2));
-            } else {
-                return -1;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in beginSendObject", e);
-            return -1;
-        }
-    }
-
-    private void endSendObject(String path, int handle, int format, boolean succeeded) {
-        if (succeeded) {
-            // handle abstract playlists separately
-            // they do not exist in the file system so don't use the media scanner here
-            if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
-                // extract name from path
-                String name = path;
-                int lastSlash = name.lastIndexOf('/');
-                if (lastSlash >= 0) {
-                    name = name.substring(lastSlash + 1);
-                }
-                // strip trailing ".pla" from the name
-                if (name.endsWith(".pla")) {
-                    name = name.substring(0, name.length() - 4);
-                }
-
-                ContentValues values = new ContentValues(1);
-                values.put(Audio.Playlists.DATA, path);
-                values.put(Audio.Playlists.NAME, name);
-                values.put(Files.FileColumns.FORMAT, format);
-                values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
-                values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
-                try {
-                    Uri uri = mMediaProvider.insert(
-                            Audio.Playlists.EXTERNAL_CONTENT_URI, values);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "RemoteException in endSendObject", e);
-                }
-            } else {
-                mMediaScanner.scanMtpFile(path, handle, format);
-            }
-        } else {
-            deleteFile(handle);
-        }
-    }
-
-    private void doScanDirectory(String path) {
-        String[] scanPath;
-        scanPath = new String[] { path };
-        mMediaScanner.scanDirectories(scanPath);
-    }
-
-    private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException {
-        String where;
-        String[] whereArgs;
-
-        if (storageID == 0xFFFFFFFF) {
-            // query all stores
-            if (format == 0) {
-                // query all formats
-                if (parent == 0) {
-                    // query all objects
-                    where = null;
-                    whereArgs = null;
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                    }
-                    where = PARENT_WHERE;
-                    whereArgs = new String[] { Integer.toString(parent) };
-                }
-            } else {
-                // query specific format
-                if (parent == 0) {
-                    // query all objects
-                    where = FORMAT_WHERE;
-                    whereArgs = new String[] { Integer.toString(format) };
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                    }
-                    where = FORMAT_PARENT_WHERE;
-                    whereArgs = new String[] { Integer.toString(format),
-                                               Integer.toString(parent) };
-                }
-            }
-        } else {
-            // query specific store
-            if (format == 0) {
-                // query all formats
-                if (parent == 0) {
-                    // query all objects
-                    where = STORAGE_WHERE;
-                    whereArgs = new String[] { Integer.toString(storageID) };
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                        where = STORAGE_PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(storageID),
-                                Integer.toString(parent)};
-                    }  else {
-                        // If a parent is specified, the storage is redundant
-                        where = PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(parent)};
-                    }
-                }
-            } else {
-                // query specific format
-                if (parent == 0) {
-                    // query all objects
-                    where = STORAGE_FORMAT_WHERE;
-                    whereArgs = new String[] {  Integer.toString(storageID),
-                                                Integer.toString(format) };
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                        where = STORAGE_FORMAT_PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(storageID),
-                                Integer.toString(format),
-                                Integer.toString(parent)};
-                    } else {
-                        // If a parent is specified, the storage is redundant
-                        where = FORMAT_PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(format),
-                                Integer.toString(parent)};
-                    }
-                }
-            }
-        }
-
-        // if we are restricting queries to mSubDirectories, we need to add the restriction
-        // onto our "where" arguments
-        if (mSubDirectoriesWhere != null) {
-            if (where == null) {
-                where = mSubDirectoriesWhere;
-                whereArgs = mSubDirectoriesWhereArgs;
-            } else {
-                where = where + " AND " + mSubDirectoriesWhere;
-
-                // create new array to hold whereArgs and mSubDirectoriesWhereArgs
-                String[] newWhereArgs =
-                        new String[whereArgs.length + mSubDirectoriesWhereArgs.length];
-                int i, j;
-                for (i = 0; i < whereArgs.length; i++) {
-                    newWhereArgs[i] = whereArgs[i];
-                }
-                for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) {
-                    newWhereArgs[i] = mSubDirectoriesWhereArgs[j];
-                }
-                whereArgs = newWhereArgs;
-            }
-        }
-
-        return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where,
-                whereArgs, null, null);
-    }
-
-    private int[] getObjectList(int storageID, int format, int parent) {
-        Cursor c = null;
-        try {
-            c = createObjectQuery(storageID, format, parent);
-            if (c == null) {
-                return null;
-            }
-            int count = c.getCount();
-            if (count > 0) {
-                int[] result = new int[count];
-                for (int i = 0; i < count; i++) {
-                    c.moveToNext();
-                    result[i] = c.getInt(0);
-                }
-                return result;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectList", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return null;
-    }
-
-    private int getNumObjects(int storageID, int format, int parent) {
-        Cursor c = null;
-        try {
-            c = createObjectQuery(storageID, format, parent);
-            if (c != null) {
-                return c.getCount();
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getNumObjects", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return -1;
-    }
-
-    private int[] getSupportedPlaybackFormats() {
-        return new int[] {
-            // allow transfering arbitrary files
+    private static final int[] PLAYBACK_FORMATS = {
+            // allow transferring arbitrary files
             MtpConstants.FORMAT_UNDEFINED,
 
             MtpConstants.FORMAT_ASSOCIATION,
@@ -613,45 +133,23 @@
             MtpConstants.FORMAT_FLAC,
             MtpConstants.FORMAT_DNG,
             MtpConstants.FORMAT_HEIF,
-        };
-    }
+    };
 
-    private int[] getSupportedCaptureFormats() {
-        // no capture formats yet
-        return null;
-    }
-
-    static final int[] FILE_PROPERTIES = {
-            // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
-            // and IMAGE_PROPERTIES below
+    private static final int[] FILE_PROPERTIES = {
             MtpConstants.PROPERTY_STORAGE_ID,
             MtpConstants.PROPERTY_OBJECT_FORMAT,
             MtpConstants.PROPERTY_PROTECTION_STATUS,
             MtpConstants.PROPERTY_OBJECT_SIZE,
             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
             MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
             MtpConstants.PROPERTY_PERSISTENT_UID,
+            MtpConstants.PROPERTY_PARENT_OBJECT,
             MtpConstants.PROPERTY_NAME,
             MtpConstants.PROPERTY_DISPLAY_NAME,
             MtpConstants.PROPERTY_DATE_ADDED,
     };
 
-    static final int[] AUDIO_PROPERTIES = {
-            // NOTE must match FILE_PROPERTIES above
-            MtpConstants.PROPERTY_STORAGE_ID,
-            MtpConstants.PROPERTY_OBJECT_FORMAT,
-            MtpConstants.PROPERTY_PROTECTION_STATUS,
-            MtpConstants.PROPERTY_OBJECT_SIZE,
-            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
-            MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
-            MtpConstants.PROPERTY_PERSISTENT_UID,
-            MtpConstants.PROPERTY_NAME,
-            MtpConstants.PROPERTY_DISPLAY_NAME,
-            MtpConstants.PROPERTY_DATE_ADDED,
-
-            // audio specific properties
+    private static final int[] AUDIO_PROPERTIES = {
             MtpConstants.PROPERTY_ARTIST,
             MtpConstants.PROPERTY_ALBUM_NAME,
             MtpConstants.PROPERTY_ALBUM_ARTIST,
@@ -667,45 +165,25 @@
             MtpConstants.PROPERTY_SAMPLE_RATE,
     };
 
-    static final int[] VIDEO_PROPERTIES = {
-            // NOTE must match FILE_PROPERTIES above
-            MtpConstants.PROPERTY_STORAGE_ID,
-            MtpConstants.PROPERTY_OBJECT_FORMAT,
-            MtpConstants.PROPERTY_PROTECTION_STATUS,
-            MtpConstants.PROPERTY_OBJECT_SIZE,
-            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
-            MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
-            MtpConstants.PROPERTY_PERSISTENT_UID,
-            MtpConstants.PROPERTY_NAME,
-            MtpConstants.PROPERTY_DISPLAY_NAME,
-            MtpConstants.PROPERTY_DATE_ADDED,
-
-            // video specific properties
+    private static final int[] VIDEO_PROPERTIES = {
             MtpConstants.PROPERTY_ARTIST,
             MtpConstants.PROPERTY_ALBUM_NAME,
             MtpConstants.PROPERTY_DURATION,
             MtpConstants.PROPERTY_DESCRIPTION,
     };
 
-    static final int[] IMAGE_PROPERTIES = {
-            // NOTE must match FILE_PROPERTIES above
-            MtpConstants.PROPERTY_STORAGE_ID,
-            MtpConstants.PROPERTY_OBJECT_FORMAT,
-            MtpConstants.PROPERTY_PROTECTION_STATUS,
-            MtpConstants.PROPERTY_OBJECT_SIZE,
-            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
-            MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
-            MtpConstants.PROPERTY_PERSISTENT_UID,
-            MtpConstants.PROPERTY_NAME,
-            MtpConstants.PROPERTY_DISPLAY_NAME,
-            MtpConstants.PROPERTY_DATE_ADDED,
-
-            // image specific properties
+    private static final int[] IMAGE_PROPERTIES = {
             MtpConstants.PROPERTY_DESCRIPTION,
     };
 
+    private static final int[] DEVICE_PROPERTIES = {
+            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
+            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
+            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
+            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
+            MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
+    };
+
     private int[] getSupportedObjectProperties(int format) {
         switch (format) {
             case MtpConstants.FORMAT_MP3:
@@ -713,183 +191,541 @@
             case MtpConstants.FORMAT_WMA:
             case MtpConstants.FORMAT_OGG:
             case MtpConstants.FORMAT_AAC:
-                return AUDIO_PROPERTIES;
+                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+                        Arrays.stream(AUDIO_PROPERTIES)).toArray();
             case MtpConstants.FORMAT_MPEG:
             case MtpConstants.FORMAT_3GP_CONTAINER:
             case MtpConstants.FORMAT_WMV:
-                return VIDEO_PROPERTIES;
+                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+                        Arrays.stream(VIDEO_PROPERTIES)).toArray();
             case MtpConstants.FORMAT_EXIF_JPEG:
             case MtpConstants.FORMAT_GIF:
             case MtpConstants.FORMAT_PNG:
             case MtpConstants.FORMAT_BMP:
             case MtpConstants.FORMAT_DNG:
             case MtpConstants.FORMAT_HEIF:
-                return IMAGE_PROPERTIES;
+                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+                        Arrays.stream(IMAGE_PROPERTIES)).toArray();
             default:
                 return FILE_PROPERTIES;
         }
     }
 
     private int[] getSupportedDeviceProperties() {
-        return new int[] {
-            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
-            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
-            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
-            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
-            MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
-        };
+        return DEVICE_PROPERTIES;
+    }
+
+    private int[] getSupportedPlaybackFormats() {
+        return PLAYBACK_FORMATS;
+    }
+
+    private int[] getSupportedCaptureFormats() {
+        // no capture formats yet
+        return null;
+    }
+
+    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
+                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
+                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+                if (newLevel != mBatteryLevel) {
+                    mBatteryLevel = newLevel;
+                    if (mServer != null) {
+                        // send device property changed event
+                        mServer.sendDevicePropertyChanged(
+                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
+                    }
+                }
+            }
+        }
+    };
+
+    public MtpDatabase(Context context, Context userContext, String volumeName,
+            String[] subDirectories) {
+        native_setup();
+        mContext = context;
+        mMediaProvider = userContext.getContentResolver()
+                .acquireContentProviderClient(MediaStore.AUTHORITY);
+        mVolumeName = volumeName;
+        mObjectsUri = Files.getMtpObjectsUri(volumeName);
+        mMediaScanner = new MediaScanner(context, mVolumeName);
+        mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
+            @Override
+            public void sendObjectAdded(int id) {
+                if (MtpDatabase.this.mServer != null)
+                    MtpDatabase.this.mServer.sendObjectAdded(id);
+            }
+
+            @Override
+            public void sendObjectRemoved(int id) {
+                if (MtpDatabase.this.mServer != null)
+                    MtpDatabase.this.mServer.sendObjectRemoved(id);
+            }
+        }, subDirectories == null ? null : Sets.newHashSet(subDirectories));
+
+        initDeviceProperties(context);
+        mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
+        mCloseGuard.open("close");
+    }
+
+    public void setServer(MtpServer server) {
+        mServer = server;
+        // always unregister before registering
+        try {
+            mContext.unregisterReceiver(mBatteryReceiver);
+        } catch (IllegalArgumentException e) {
+            // wasn't previously registered, ignore
+        }
+        // register for battery notifications when we are connected
+        if (server != null) {
+            mContext.registerReceiver(mBatteryReceiver,
+                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+        }
+    }
+
+    @Override
+    public void close() {
+        mManager.close();
+        mCloseGuard.close();
+        if (mClosed.compareAndSet(false, true)) {
+            mMediaScanner.close();
+            mMediaProvider.close();
+            native_finalize();
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    public void addStorage(StorageVolume storage) {
+        MtpStorage mtpStorage = mManager.addMtpStorage(storage);
+        mStorageMap.put(storage.getPath(), mtpStorage);
+        mServer.addStorage(mtpStorage);
+    }
+
+    public void removeStorage(StorageVolume storage) {
+        MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
+        if (mtpStorage == null) {
+            return;
+        }
+        mServer.removeStorage(mtpStorage);
+        mManager.removeMtpStorage(mtpStorage);
+        mStorageMap.remove(storage.getPath());
+    }
+
+    private void initDeviceProperties(Context context) {
+        final String devicePropertiesName = "device-properties";
+        mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
+                Context.MODE_PRIVATE);
+        File databaseFile = context.getDatabasePath(devicePropertiesName);
+
+        if (databaseFile.exists()) {
+            // for backward compatibility - read device properties from sqlite database
+            // and migrate them to shared prefs
+            SQLiteDatabase db = null;
+            Cursor c = null;
+            try {
+                db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
+                if (db != null) {
+                    c = db.query("properties", new String[]{"_id", "code", "value"},
+                            null, null, null, null, null);
+                    if (c != null) {
+                        SharedPreferences.Editor e = mDeviceProperties.edit();
+                        while (c.moveToNext()) {
+                            String name = c.getString(1);
+                            String value = c.getString(2);
+                            e.putString(name, value);
+                        }
+                        e.commit();
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "failed to migrate device properties", e);
+            } finally {
+                if (c != null) c.close();
+                if (db != null) db.close();
+            }
+            context.deleteDatabase(devicePropertiesName);
+        }
+    }
+
+    private int beginSendObject(String path, int format, int parent, int storageId) {
+        MtpStorageManager.MtpObject parentObj =
+                parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
+        if (parentObj == null) {
+            return -1;
+        }
+
+        Path objPath = Paths.get(path);
+        return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
+    }
+
+    private void endSendObject(int handle, boolean succeeded) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null || !mManager.endSendObject(obj, succeeded)) {
+            Log.e(TAG, "Failed to successfully end send object");
+            return;
+        }
+        // Add the new file to MediaProvider
+        if (succeeded) {
+            String path = obj.getPath().toString();
+            int format = obj.getFormat();
+            // Get parent info from MediaProvider, since the id is different from MTP's
+            ContentValues values = new ContentValues();
+            values.put(Files.FileColumns.DATA, path);
+            values.put(Files.FileColumns.FORMAT, format);
+            values.put(Files.FileColumns.SIZE, obj.getSize());
+            values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+            try {
+                if (obj.getParent().isRoot()) {
+                    values.put(Files.FileColumns.PARENT, 0);
+                } else {
+                    int parentId = findInMedia(obj.getParent().getPath());
+                    if (parentId != -1) {
+                        values.put(Files.FileColumns.PARENT, parentId);
+                    } else {
+                        // The parent isn't in MediaProvider. Don't add the new file.
+                        return;
+                    }
+                }
+
+                Uri uri = mMediaProvider.insert(mObjectsUri, values);
+                if (uri != null) {
+                    rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in beginSendObject", e);
+            }
+        }
+    }
+
+    private void rescanFile(String path, int handle, int format) {
+        // handle abstract playlists separately
+        // they do not exist in the file system so don't use the media scanner here
+        if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
+            // extract name from path
+            String name = path;
+            int lastSlash = name.lastIndexOf('/');
+            if (lastSlash >= 0) {
+                name = name.substring(lastSlash + 1);
+            }
+            // strip trailing ".pla" from the name
+            if (name.endsWith(".pla")) {
+                name = name.substring(0, name.length() - 4);
+            }
+
+            ContentValues values = new ContentValues(1);
+            values.put(Audio.Playlists.DATA, path);
+            values.put(Audio.Playlists.NAME, name);
+            values.put(Files.FileColumns.FORMAT, format);
+            values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
+            values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
+            try {
+                mMediaProvider.insert(
+                        Audio.Playlists.EXTERNAL_CONTENT_URI, values);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in endSendObject", e);
+            }
+        } else {
+            mMediaScanner.scanMtpFile(path, handle, format);
+        }
+    }
+
+    private int[] getObjectList(int storageID, int format, int parent) {
+        Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
+                format, storageID);
+        if (objectStream == null) {
+            return null;
+        }
+        return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray();
+    }
+
+    private int getNumObjects(int storageID, int format, int parent) {
+        Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
+                format, storageID);
+        if (objectStream == null) {
+            return -1;
+        }
+        return (int) objectStream.count();
     }
 
     private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
-                        int groupCode, int depth) {
+            int groupCode, int depth) {
         // FIXME - implement group support
-        if (groupCode != 0) {
-            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
+        if (property == 0) {
+            if (groupCode == 0) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
+            }
+            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
+        }
+        if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
+            // request all objects starting at root
+            handle = 0xFFFFFFFF;
+            depth = 0;
+        }
+        if (!(depth == 0 || depth == 1)) {
+            // we only support depth 0 and 1
+            // depth 0: single object, depth 1: immediate children
+            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
+        }
+        Stream<MtpStorageManager.MtpObject> objectStream = Stream.of();
+        if (handle == 0xFFFFFFFF) {
+            // All objects are requested
+            objectStream = mManager.getObjects(0, format, 0xFFFFFFFF);
+            if (objectStream == null) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+            }
+        } else if (handle != 0) {
+            // Add the requested object if format matches
+            MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+            if (obj == null) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+            }
+            if (obj.getFormat() == format || format == 0) {
+                objectStream = Stream.of(obj);
+            }
+        }
+        if (handle == 0 || depth == 1) {
+            if (handle == 0) {
+                handle = 0xFFFFFFFF;
+            }
+            // Get the direct children of root or this object.
+            Stream<MtpStorageManager.MtpObject> childStream = mManager.getObjects(handle, format,
+                    0xFFFFFFFF);
+            if (childStream == null) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+            }
+            objectStream = Stream.concat(objectStream, childStream);
         }
 
+        MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
         MtpPropertyGroup propertyGroup;
-        if (property == 0xffffffff) {
-            if (format == 0 && handle != 0 && handle != 0xffffffff) {
-                // return properties based on the object's format
-                format = getObjectFormat(handle);
+        Iterator<MtpStorageManager.MtpObject> iter = objectStream.iterator();
+        while (iter.hasNext()) {
+            MtpStorageManager.MtpObject obj = iter.next();
+            if (property == 0xffffffff) {
+                // Get all properties supported by this object
+                propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat());
+                if (propertyGroup == null) {
+                    int[] propertyList = getSupportedObjectProperties(format);
+                    propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
+                            propertyList);
+                    mPropertyGroupsByFormat.put(format, propertyGroup);
+                }
+            } else {
+                // Get this property value
+                final int[] propertyList = new int[]{property};
+                propertyGroup = mPropertyGroupsByProperty.get(property);
+                if (propertyGroup == null) {
+                    propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
+                            propertyList);
+                    mPropertyGroupsByProperty.put(property, propertyGroup);
+                }
             }
-            propertyGroup = mPropertyGroupsByFormat.get(format);
-            if (propertyGroup == null) {
-                int[] propertyList = getSupportedObjectProperties(format);
-                propertyGroup = new MtpPropertyGroup(this, mMediaProvider,
-                        mVolumeName, propertyList);
-                mPropertyGroupsByFormat.put(format, propertyGroup);
-            }
-        } else {
-            propertyGroup = mPropertyGroupsByProperty.get(property);
-            if (propertyGroup == null) {
-                final int[] propertyList = new int[] { property };
-                propertyGroup = new MtpPropertyGroup(
-                        this, mMediaProvider, mVolumeName, propertyList);
-                mPropertyGroupsByProperty.put(property, propertyGroup);
+            int err = propertyGroup.getPropertyList(obj, ret);
+            if (err != MtpConstants.RESPONSE_OK) {
+                return new MtpPropertyList(err);
             }
         }
-
-        return propertyGroup.getPropertyList(handle, format, depth);
+        return ret;
     }
 
     private int renameFile(int handle, String newName) {
-        Cursor c = null;
-
-        // first compute current path
-        String path = null;
-        String[] whereArgs = new String[] {  Integer.toString(handle) };
-        try {
-            c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE,
-                    whereArgs, null, null);
-            if (c != null && c.moveToNext()) {
-                path = c.getString(1);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectFilePath", e);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        if (path == null) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
         }
-
-        // do not allow renaming any of the special subdirectories
-        if (isStorageSubDirectory(path)) {
-            return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
-        }
+        Path oldPath = obj.getPath();
 
         // now rename the file.  make sure this succeeds before updating database
-        File oldFile = new File(path);
-        int lastSlash = path.lastIndexOf('/');
-        if (lastSlash <= 1) {
+        if (!mManager.beginRenameObject(obj, newName))
             return MtpConstants.RESPONSE_GENERAL_ERROR;
+        Path newPath = obj.getPath();
+        boolean success = oldPath.toFile().renameTo(newPath.toFile());
+        if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
+            Log.e(TAG, "Failed to end rename object");
         }
-        String newPath = path.substring(0, lastSlash + 1) + newName;
-        File newFile = new File(newPath);
-        boolean success = oldFile.renameTo(newFile);
         if (!success) {
-            Log.w(TAG, "renaming "+ path + " to " + newPath + " failed");
             return MtpConstants.RESPONSE_GENERAL_ERROR;
         }
 
-        // finally update database
+        // finally update MediaProvider
         ContentValues values = new ContentValues();
-        values.put(Files.FileColumns.DATA, newPath);
-        int updated = 0;
+        values.put(Files.FileColumns.DATA, newPath.toString());
+        String[] whereArgs = new String[]{oldPath.toString()};
         try {
             // note - we are relying on a special case in MediaProvider.update() to update
             // the paths for all children in the case where this is a directory.
-            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
+            mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
         }
-        if (updated == 0) {
-            Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
-            // this shouldn't happen, but if it does we need to rename the file to its original name
-            newFile.renameTo(oldFile);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        }
 
         // check if nomedia status changed
-        if (newFile.isDirectory()) {
+        if (obj.isDir()) {
             // for directories, check if renamed from something hidden to something non-hidden
-            if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) {
+            if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
                 // directory was unhidden
                 try {
-                    mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath, null);
+                    mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null);
                 } catch (RemoteException e) {
                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
                 }
             }
         } else {
             // for files, check if renamed from .nomedia to something else
-            if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia")
-                    && !newPath.toLowerCase(Locale.US).equals(".nomedia")) {
+            if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
+                    && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
                 try {
-                    mMediaProvider.call(MediaStore.UNHIDE_CALL, oldFile.getParent(), null);
+                    mMediaProvider.call(MediaStore.UNHIDE_CALL,
+                            oldPath.getParent().toString(), null);
                 } catch (RemoteException e) {
                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
                 }
             }
         }
-
         return MtpConstants.RESPONSE_OK;
     }
 
-    private int moveObject(int handle, int newParent, int newStorage, String newPath) {
-        String[] whereArgs = new String[] {  Integer.toString(handle) };
+    private int beginMoveObject(int handle, int newParent, int newStorage) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        MtpStorageManager.MtpObject parent = newParent == 0 ?
+                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+        if (obj == null || parent == null)
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
 
-        // do not allow renaming any of the special subdirectories
-        if (isStorageSubDirectory(newPath)) {
-            return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
+        boolean allowed = mManager.beginMoveObject(obj, parent);
+        return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
+    }
+
+    private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
+            int objId, boolean success) {
+        MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
+                mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
+        MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
+                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+        MtpStorageManager.MtpObject obj = mManager.getObject(objId);
+        String name = obj.getName();
+        if (newParentObj == null || oldParentObj == null
+                ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
+            Log.e(TAG, "Failed to end move object");
+            return;
         }
 
-        // update database
+        obj = mManager.getObject(objId);
+        if (!success || obj == null)
+            return;
+        // Get parent info from MediaProvider, since the id is different from MTP's
         ContentValues values = new ContentValues();
-        values.put(Files.FileColumns.DATA, newPath);
-        values.put(Files.FileColumns.PARENT, newParent);
-        values.put(Files.FileColumns.STORAGE_ID, newStorage);
-        int updated = 0;
+        Path path = newParentObj.getPath().resolve(name);
+        Path oldPath = oldParentObj.getPath().resolve(name);
+        values.put(Files.FileColumns.DATA, path.toString());
+        if (obj.getParent().isRoot()) {
+            values.put(Files.FileColumns.PARENT, 0);
+        } else {
+            int parentId = findInMedia(path.getParent());
+            if (parentId != -1) {
+                values.put(Files.FileColumns.PARENT, parentId);
+            } else {
+                // The new parent isn't in MediaProvider, so delete the object instead
+                deleteFromMedia(oldPath, obj.isDir());
+                return;
+            }
+        }
+        // update MediaProvider
+        Cursor c = null;
+        String[] whereArgs = new String[]{oldPath.toString()};
         try {
-            // note - we are relying on a special case in MediaProvider.update() to update
-            // the paths for all children in the case where this is a directory.
-            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
+            int parentId = -1;
+            if (!oldParentObj.isRoot()) {
+                parentId = findInMedia(oldPath.getParent());
+            }
+            if (oldParentObj.isRoot() || parentId != -1) {
+                // Old parent exists in MediaProvider - perform a move
+                // note - we are relying on a special case in MediaProvider.update() to update
+                // the paths for all children in the case where this is a directory.
+                mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
+            } else {
+                // Old parent doesn't exist - add the object
+                values.put(Files.FileColumns.FORMAT, obj.getFormat());
+                values.put(Files.FileColumns.SIZE, obj.getSize());
+                values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+                Uri uri = mMediaProvider.insert(mObjectsUri, values);
+                if (uri != null) {
+                    rescanFile(path.toString(),
+                            Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat());
+                }
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
         }
-        if (updated == 0) {
-            Log.e(TAG, "Unable to update path for " + handle + " to " + newPath);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
+    }
+
+    private int beginCopyObject(int handle, int newParent, int newStorage) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        MtpStorageManager.MtpObject parent = newParent == 0 ?
+                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+        if (obj == null || parent == null)
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+        return mManager.beginCopyObject(obj, parent);
+    }
+
+    private void endCopyObject(int handle, boolean success) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null || !mManager.endCopyObject(obj, success)) {
+            Log.e(TAG, "Failed to end copy object");
+            return;
         }
-        return MtpConstants.RESPONSE_OK;
+        if (!success) {
+            return;
+        }
+        String path = obj.getPath().toString();
+        int format = obj.getFormat();
+        // Get parent info from MediaProvider, since the id is different from MTP's
+        ContentValues values = new ContentValues();
+        values.put(Files.FileColumns.DATA, path);
+        values.put(Files.FileColumns.FORMAT, format);
+        values.put(Files.FileColumns.SIZE, obj.getSize());
+        values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+        try {
+            if (obj.getParent().isRoot()) {
+                values.put(Files.FileColumns.PARENT, 0);
+            } else {
+                int parentId = findInMedia(obj.getParent().getPath());
+                if (parentId != -1) {
+                    values.put(Files.FileColumns.PARENT, parentId);
+                } else {
+                    // The parent isn't in MediaProvider. Don't add the new file.
+                    return;
+                }
+            }
+            if (obj.isDir()) {
+                mMediaScanner.scanDirectories(new String[]{path});
+            } else {
+                Uri uri = mMediaProvider.insert(mObjectsUri, values);
+                if (uri != null) {
+                    rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
+                }
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in beginSendObject", e);
+        }
     }
 
     private int setObjectProperty(int handle, int property,
-                            long intValue, String stringValue) {
+            long intValue, String stringValue) {
         switch (property) {
             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
                 return renameFile(handle, stringValue);
@@ -912,24 +748,23 @@
                 value.getChars(0, length, outStringValue, 0);
                 outStringValue[length] = 0;
                 return MtpConstants.RESPONSE_OK;
-
             case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
                 // use screen size as max image size
-                Display display = ((WindowManager)mContext.getSystemService(
+                Display display = ((WindowManager) mContext.getSystemService(
                         Context.WINDOW_SERVICE)).getDefaultDisplay();
                 int width = display.getMaximumSizeDimension();
                 int height = display.getMaximumSizeDimension();
-                String imageSize = Integer.toString(width) + "x" +  Integer.toString(height);
+                String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
                 imageSize.getChars(0, imageSize.length(), outStringValue, 0);
                 outStringValue[imageSize.length()] = 0;
                 return MtpConstants.RESPONSE_OK;
-
             case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
                 outIntValue[0] = mDeviceType;
                 return MtpConstants.RESPONSE_OK;
-
-            // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code
-
+            case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
+                outIntValue[0] = mBatteryLevel;
+                outIntValue[1] = mBatteryScale;
+                return MtpConstants.RESPONSE_OK;
             default:
                 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
         }
@@ -950,179 +785,144 @@
     }
 
     private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
-                        char[] outName, long[] outCreatedModified) {
-        Cursor c = null;
-        try {
-            c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
-                            ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
-            if (c != null && c.moveToNext()) {
-                outStorageFormatParent[0] = c.getInt(1);
-                outStorageFormatParent[1] = c.getInt(2);
-                outStorageFormatParent[2] = c.getInt(3);
-
-                // extract name from path
-                String path = c.getString(4);
-                int lastSlash = path.lastIndexOf('/');
-                int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
-                int end = path.length();
-                if (end - start > 255) {
-                    end = start + 255;
-                }
-                path.getChars(start, end, outName, 0);
-                outName[end - start] = 0;
-
-                outCreatedModified[0] = c.getLong(5);
-                outCreatedModified[1] = c.getLong(6);
-                // use modification date as creation date if date added is not set
-                if (outCreatedModified[0] == 0) {
-                    outCreatedModified[0] = outCreatedModified[1];
-                }
-                return true;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectInfo", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
+            char[] outName, long[] outCreatedModified) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return false;
         }
-        return false;
+        outStorageFormatParent[0] = obj.getStorageId();
+        outStorageFormatParent[1] = obj.getFormat();
+        outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();
+
+        int nameLen = Integer.min(obj.getName().length(), 255);
+        obj.getName().getChars(0, nameLen, outName, 0);
+        outName[nameLen] = 0;
+
+        outCreatedModified[0] = obj.getModifiedTime();
+        outCreatedModified[1] = obj.getModifiedTime();
+        return true;
     }
 
     private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
-        if (handle == 0) {
-            // special case root directory
-            mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
-            outFilePath[mMediaStoragePath.length()] = 0;
-            outFileLengthFormat[0] = 0;
-            outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
-            return MtpConstants.RESPONSE_OK;
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
         }
-        Cursor c = null;
-        try {
-            c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION,
-                            ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
-            if (c != null && c.moveToNext()) {
-                String path = c.getString(1);
-                path.getChars(0, path.length(), outFilePath, 0);
-                outFilePath[path.length()] = 0;
-                // File transfers from device to host will likely fail if the size is incorrect.
-                // So to be safe, use the actual file size here.
-                outFileLengthFormat[0] = new File(path).length();
-                outFileLengthFormat[1] = c.getLong(2);
-                return MtpConstants.RESPONSE_OK;
-            } else {
-                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectFilePath", e);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
+
+        String path = obj.getPath().toString();
+        int pathLen = Integer.min(path.length(), 4096);
+        path.getChars(0, pathLen, outFilePath, 0);
+        outFilePath[pathLen] = 0;
+
+        outFileLengthFormat[0] = obj.getSize();
+        outFileLengthFormat[1] = obj.getFormat();
+        return MtpConstants.RESPONSE_OK;
     }
 
     private int getObjectFormat(int handle) {
-        Cursor c = null;
-        try {
-            c = mMediaProvider.query(mObjectsUri, FORMAT_PROJECTION,
-                            ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
-            if (c != null && c.moveToNext()) {
-                return c.getInt(1);
-            } else {
-                return -1;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectFilePath", e);
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
             return -1;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
         }
+        return obj.getFormat();
     }
 
-    private int deleteFile(int handle) {
-        mDatabaseModified = true;
-        String path = null;
-        int format = 0;
+    private int beginDeleteObject(int handle) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+        }
+        if (!mManager.beginRemoveObject(obj)) {
+            return MtpConstants.RESPONSE_GENERAL_ERROR;
+        }
+        return MtpConstants.RESPONSE_OK;
+    }
 
+    private void endDeleteObject(int handle, boolean success) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return;
+        }
+        if (!mManager.endRemoveObject(obj, success))
+            Log.e(TAG, "Failed to end remove object");
+        if (success)
+            deleteFromMedia(obj.getPath(), obj.isDir());
+    }
+
+    private int findInMedia(Path path) {
+        int ret = -1;
         Cursor c = null;
         try {
-            c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION,
-                            ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
+            c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
+                    new String[]{path.toString()}, null, null);
             if (c != null && c.moveToNext()) {
-                // don't convert to media path here, since we will be matching
-                // against paths in the database matching /data/media
-                path = c.getString(1);
-                format = c.getInt(2);
-            } else {
-                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+                ret = c.getInt(0);
             }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error finding " + path + " in MediaProvider");
+        } finally {
+            if (c != null)
+                c.close();
+        }
+        return ret;
+    }
 
-            if (path == null || format == 0) {
-                return MtpConstants.RESPONSE_GENERAL_ERROR;
-            }
-
-            // do not allow deleting any of the special subdirectories
-            if (isStorageSubDirectory(path)) {
-                return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
-            }
-
-            if (format == MtpConstants.FORMAT_ASSOCIATION) {
+    private void deleteFromMedia(Path path, boolean isDir) {
+        try {
+            // Delete the object(s) from MediaProvider, but ignore errors.
+            if (isDir) {
                 // recursive case - delete all children first
-                Uri uri = Files.getMtpObjectsUri(mVolumeName);
-                int count = mMediaProvider.delete(uri,
-                    // the 'like' makes it use the index, the 'lower()' makes it correct
-                    // when the path contains sqlite wildcard characters
-                    "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
-                    new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"});
+                mMediaProvider.delete(mObjectsUri,
+                        // the 'like' makes it use the index, the 'lower()' makes it correct
+                        // when the path contains sqlite wildcard characters
+                        "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
+                        new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
+                                path.toString() + "/"});
             }
 
-            Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
-            if (mMediaProvider.delete(uri, null, null) > 0) {
-                if (format != MtpConstants.FORMAT_ASSOCIATION
-                        && path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
+            String[] whereArgs = new String[]{path.toString()};
+            if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) {
+                if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
                     try {
-                        String parentPath = path.substring(0, path.lastIndexOf("/"));
+                        String parentPath = path.getParent().toString();
                         mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null);
                     } catch (RemoteException e) {
                         Log.e(TAG, "failed to unhide/rescan for " + path);
                     }
                 }
-                return MtpConstants.RESPONSE_OK;
             } else {
-                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+                Log.i(TAG, "Mediaprovider didn't delete " + path);
             }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in deleteFile", e);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
+        } catch (Exception e) {
+            Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
         }
     }
 
     private int[] getObjectReferences(int handle) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null)
+            return null;
+        // Translate this handle to the MediaProvider Handle
+        handle = findInMedia(obj.getPath());
+        if (handle == -1)
+            return null;
         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
         Cursor c = null;
         try {
-            c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null);
+            c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null);
             if (c == null) {
                 return null;
             }
-            int count = c.getCount();
-            if (count > 0) {
-                int[] result = new int[count];
-                for (int i = 0; i < count; i++) {
-                    c.moveToNext();
-                    result[i] = c.getInt(0);
+                ArrayList<Integer> result = new ArrayList<>();
+                while (c.moveToNext()) {
+                    // Translate result handles back into handles for this session.
+                    String refPath = c.getString(0);
+                    MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath);
+                    if (refObj != null) {
+                        result.add(refObj.getId());
+                    }
                 }
-                return result;
-            }
+                return result.stream().mapToInt(Integer::intValue).toArray();
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in getObjectList", e);
         } finally {
@@ -1134,17 +934,29 @@
     }
 
     private int setObjectReferences(int handle, int[] references) {
-        mDatabaseModified = true;
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null)
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+        // Translate this handle to the MediaProvider Handle
+        handle = findInMedia(obj.getPath());
+        if (handle == -1)
+            return MtpConstants.RESPONSE_GENERAL_ERROR;
         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
-        int count = references.length;
-        ContentValues[] valuesList = new ContentValues[count];
-        for (int i = 0; i < count; i++) {
+        ArrayList<ContentValues> valuesList = new ArrayList<>();
+        for (int id : references) {
+            // Translate each reference id to the MediaProvider Id
+            MtpStorageManager.MtpObject refObj = mManager.getObject(id);
+            if (refObj == null)
+                continue;
+            int refHandle = findInMedia(refObj.getPath());
+            if (refHandle == -1)
+                continue;
             ContentValues values = new ContentValues();
-            values.put(Files.FileColumns._ID, references[i]);
-            valuesList[i] = values;
+            values.put(Files.FileColumns._ID, refHandle);
+            valuesList.add(values);
         }
         try {
-            if (mMediaProvider.bulkInsert(uri, valuesList) > 0) {
+            if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) {
                 return MtpConstants.RESPONSE_OK;
             }
         } catch (RemoteException e) {
@@ -1153,17 +965,6 @@
         return MtpConstants.RESPONSE_GENERAL_ERROR;
     }
 
-    private void sessionStarted() {
-        mDatabaseModified = false;
-    }
-
-    private void sessionEnded() {
-        if (mDatabaseModified) {
-            mUserContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
-            mDatabaseModified = false;
-        }
-    }
-
     // used by the JNI code
     private long mNativeContext;
 
diff --git a/media/java/android/mtp/MtpPropertyGroup.java b/media/java/android/mtp/MtpPropertyGroup.java
index dea3008..77d0f34f 100644
--- a/media/java/android/mtp/MtpPropertyGroup.java
+++ b/media/java/android/mtp/MtpPropertyGroup.java
@@ -23,22 +23,21 @@
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Files;
 import android.provider.MediaStore.Images;
-import android.provider.MediaStore.MediaColumns;
 import android.util.Log;
 
 import java.util.ArrayList;
 
+/**
+ * MtpPropertyGroup represents a list of MTP properties.
+ * {@hide}
+ */
 class MtpPropertyGroup {
-
-    private static final String TAG = "MtpPropertyGroup";
+    private static final String TAG = MtpPropertyGroup.class.getSimpleName();
 
     private class Property {
-        // MTP property code
-        int     code;
-        // MTP data type
-        int     type;
-        // column index for our query
-        int     column;
+        int code;
+        int type;
+        int column;
 
         Property(int code, int type, int column) {
             this.code = code;
@@ -47,32 +46,26 @@
         }
     }
 
-    private final MtpDatabase mDatabase;
     private final ContentProviderClient mProvider;
     private final String mVolumeName;
     private final Uri mUri;
 
     // list of all properties in this group
-    private final Property[]    mProperties;
+    private final Property[] mProperties;
 
     // list of columns for database query
-    private String[]             mColumns;
+    private String[] mColumns;
 
-    private static final String ID_WHERE = Files.FileColumns._ID + "=?";
-    private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
-    private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE;
-    private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
-    private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE;
+    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
+
     // constructs a property group for a list of properties
-    public MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName,
-            int[] properties) {
-        mDatabase = database;
+    public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) {
         mProvider = provider;
         mVolumeName = volumeName;
         mUri = Files.getMtpObjectsUri(volumeName);
 
         int count = properties.length;
-        ArrayList<String> columns = new ArrayList<String>(count);
+        ArrayList<String> columns = new ArrayList<>(count);
         columns.add(Files.FileColumns._ID);
 
         mProperties = new Property[count];
@@ -90,37 +83,29 @@
         String column = null;
         int type;
 
-         switch (code) {
+        switch (code) {
             case MtpConstants.PROPERTY_STORAGE_ID:
-                column = Files.FileColumns.STORAGE_ID;
                 type = MtpConstants.TYPE_UINT32;
                 break;
-             case MtpConstants.PROPERTY_OBJECT_FORMAT:
-                column = Files.FileColumns.FORMAT;
+            case MtpConstants.PROPERTY_OBJECT_FORMAT:
                 type = MtpConstants.TYPE_UINT16;
                 break;
             case MtpConstants.PROPERTY_PROTECTION_STATUS:
-                // protection status is always 0
                 type = MtpConstants.TYPE_UINT16;
                 break;
             case MtpConstants.PROPERTY_OBJECT_SIZE:
-                column = Files.FileColumns.SIZE;
                 type = MtpConstants.TYPE_UINT64;
                 break;
             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
-                column = Files.FileColumns.DATA;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_NAME:
-                column = MediaColumns.TITLE;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_DATE_MODIFIED:
-                column = Files.FileColumns.DATE_MODIFIED;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_DATE_ADDED:
-                column = Files.FileColumns.DATE_ADDED;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
@@ -128,12 +113,9 @@
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_PARENT_OBJECT:
-                column = Files.FileColumns.PARENT;
                 type = MtpConstants.TYPE_UINT32;
                 break;
             case MtpConstants.PROPERTY_PERSISTENT_UID:
-                // PUID is concatenation of storageID and object handle
-                column = Files.FileColumns.STORAGE_ID;
                 type = MtpConstants.TYPE_UINT128;
                 break;
             case MtpConstants.PROPERTY_DURATION:
@@ -145,7 +127,6 @@
                 type = MtpConstants.TYPE_UINT16;
                 break;
             case MtpConstants.PROPERTY_DISPLAY_NAME:
-                column = MediaColumns.DISPLAY_NAME;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_ARTIST:
@@ -195,40 +176,19 @@
         }
     }
 
-   private String queryString(int id, String column) {
-        Cursor c = null;
-        try {
-            // for now we are only reading properties from the "objects" table
-            c = mProvider.query(mUri,
-                            new String [] { Files.FileColumns._ID, column },
-                            ID_WHERE, new String[] { Integer.toString(id) }, null, null);
-            if (c != null && c.moveToNext()) {
-                return c.getString(1);
-            } else {
-                return "";
-            }
-        } catch (Exception e) {
-            return null;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-    }
-
-    private String queryAudio(int id, String column) {
+    private String queryAudio(String path, String column) {
         Cursor c = null;
         try {
             c = mProvider.query(Audio.Media.getContentUri(mVolumeName),
-                            new String [] { Files.FileColumns._ID, column },
-                            ID_WHERE, new String[] { Integer.toString(id) }, null, null);
+                            new String [] { column },
+                            PATH_WHERE, new String[] {path}, null, null);
             if (c != null && c.moveToNext()) {
-                return c.getString(1);
+                return c.getString(0);
             } else {
                 return "";
             }
         } catch (Exception e) {
-            return null;
+            return "";
         } finally {
             if (c != null) {
                 c.close();
@@ -236,21 +196,19 @@
         }
     }
 
-    private String queryGenre(int id) {
+    private String queryGenre(String path) {
         Cursor c = null;
         try {
-            Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id);
-            c = mProvider.query(uri,
-                            new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME },
-                            null, null, null, null);
+            c = mProvider.query(Audio.Genres.getContentUri(mVolumeName),
+                            new String [] { Audio.GenresColumns.NAME },
+                            PATH_WHERE, new String[] {path}, null, null);
             if (c != null && c.moveToNext()) {
-                return c.getString(1);
+                return c.getString(0);
             } else {
                 return "";
             }
         } catch (Exception e) {
-            Log.e(TAG, "queryGenre exception", e);
-            return null;
+            return "";
         } finally {
             if (c != null) {
                 c.close();
@@ -258,211 +216,127 @@
         }
     }
 
-    private Long queryLong(int id, String column) {
+    /**
+     * Gets the values of the properties represented by this property group for the given
+     * object and adds them to the given property list.
+     * @return Response_OK if the operation succeeded.
+     */
+    public int getPropertyList(MtpStorageManager.MtpObject object, MtpPropertyList list) {
         Cursor c = null;
-        try {
-            // for now we are only reading properties from the "objects" table
-            c = mProvider.query(mUri,
-                            new String [] { Files.FileColumns._ID, column },
-                            ID_WHERE, new String[] { Integer.toString(id) }, null, null);
-            if (c != null && c.moveToNext()) {
-                return new Long(c.getLong(1));
-            }
-        } catch (Exception e) {
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return null;
-    }
-
-    private static String nameFromPath(String path) {
-        // extract name from full path
-        int start = 0;
-        int lastSlash = path.lastIndexOf('/');
-        if (lastSlash >= 0) {
-            start = lastSlash + 1;
-        }
-        int end = path.length();
-        if (end - start > 255) {
-            end = start + 255;
-        }
-        return path.substring(start, end);
-    }
-
-    MtpPropertyList getPropertyList(int handle, int format, int depth) {
-        //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth);
-        if (depth > 1) {
-            // we only support depth 0 and 1
-            // depth 0: single object, depth 1: immediate children
-            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
-        }
-
-        String where;
-        String[] whereArgs;
-        if (format == 0) {
-            if (handle == 0xFFFFFFFF) {
-                // select all objects
-                where = null;
-                whereArgs = null;
-            } else {
-                whereArgs = new String[] { Integer.toString(handle) };
-                if (depth == 1) {
-                    where = PARENT_WHERE;
-                } else {
-                    where = ID_WHERE;
+        int id = object.getId();
+        String path = object.getPath().toString();
+        for (Property property : mProperties) {
+            if (property.column != -1 && c == null) {
+                try {
+                    // Look up the entry in MediaProvider only if one of those properties is needed.
+                    c = mProvider.query(mUri, mColumns,
+                            PATH_WHERE, new String[] {path}, null, null);
+                    if (c != null && !c.moveToNext()) {
+                        c.close();
+                        c = null;
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Mediaprovider lookup failed");
                 }
             }
-        } else {
-            if (handle == 0xFFFFFFFF) {
-                // select all objects with given format
-                where = FORMAT_WHERE;
-                whereArgs = new String[] { Integer.toString(format) };
-            } else {
-                whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) };
-                if (depth == 1) {
-                    where = PARENT_FORMAT_WHERE;
-                } else {
-                    where = ID_FORMAT_WHERE;
-                }
-            }
-        }
-
-        Cursor c = null;
-        try {
-            // don't query if not necessary
-            if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) {
-                c = mProvider.query(mUri, mColumns, where, whereArgs, null, null);
-                if (c == null) {
-                    return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                }
-            }
-
-            int count = (c == null ? 1 : c.getCount());
-            MtpPropertyList result = new MtpPropertyList(count * mProperties.length,
-                    MtpConstants.RESPONSE_OK);
-
-            // iterate over all objects in the query
-            for (int objectIndex = 0; objectIndex < count; objectIndex++) {
-                if (c != null) {
-                    c.moveToNext();
-                    handle = (int)c.getLong(0);
-                }
-
-                // iterate over all properties in the query for the given object
-                for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) {
-                    Property property = mProperties[propertyIndex];
-                    int propertyCode = property.code;
-                    int column = property.column;
-
-                    // handle some special cases
-                    switch (propertyCode) {
-                        case MtpConstants.PROPERTY_PROTECTION_STATUS:
-                            // protection status is always 0
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
+            switch (property.code) {
+                case MtpConstants.PROPERTY_PROTECTION_STATUS:
+                    // protection status is always 0
+                    list.append(id, property.code, property.type, 0);
+                    break;
+                case MtpConstants.PROPERTY_NAME:
+                case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
+                case MtpConstants.PROPERTY_DISPLAY_NAME:
+                    list.append(id, property.code, object.getName());
+                    break;
+                case MtpConstants.PROPERTY_DATE_MODIFIED:
+                case MtpConstants.PROPERTY_DATE_ADDED:
+                    // convert from seconds to DateTime
+                    list.append(id, property.code,
+                            format_date_time(object.getModifiedTime()));
+                    break;
+                case MtpConstants.PROPERTY_STORAGE_ID:
+                    list.append(id, property.code, property.type, object.getStorageId());
+                    break;
+                case MtpConstants.PROPERTY_OBJECT_FORMAT:
+                    list.append(id, property.code, property.type, object.getFormat());
+                    break;
+                case MtpConstants.PROPERTY_OBJECT_SIZE:
+                    list.append(id, property.code, property.type, object.getSize());
+                    break;
+                case MtpConstants.PROPERTY_PARENT_OBJECT:
+                    list.append(id, property.code, property.type,
+                            object.getParent().isRoot() ? 0 : object.getParent().getId());
+                    break;
+                case MtpConstants.PROPERTY_PERSISTENT_UID:
+                    // The persistent uid must be unique and never reused among all objects,
+                    // and remain the same between sessions.
+                    long puid = (object.getPath().toString().hashCode() << 32)
+                            + object.getModifiedTime();
+                    list.append(id, property.code, property.type, puid);
+                    break;
+                case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
+                    // release date is stored internally as just the year
+                    int year = 0;
+                    if (c != null)
+                        year = c.getInt(property.column);
+                    String dateTime = Integer.toString(year) + "0101T000000";
+                    list.append(id, property.code, dateTime);
+                    break;
+                case MtpConstants.PROPERTY_TRACK:
+                    int track = 0;
+                    if (c != null)
+                        track = c.getInt(property.column);
+                    list.append(id, property.code, MtpConstants.TYPE_UINT16,
+                            track % 1000);
+                    break;
+                case MtpConstants.PROPERTY_ARTIST:
+                    list.append(id, property.code,
+                            queryAudio(path, Audio.AudioColumns.ARTIST));
+                    break;
+                case MtpConstants.PROPERTY_ALBUM_NAME:
+                    list.append(id, property.code,
+                            queryAudio(path, Audio.AudioColumns.ALBUM));
+                    break;
+                case MtpConstants.PROPERTY_GENRE:
+                    String genre = queryGenre(path);
+                    if (genre != null) {
+                        list.append(id, property.code, genre);
+                    }
+                    break;
+                case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
+                case MtpConstants.PROPERTY_AUDIO_BITRATE:
+                case MtpConstants.PROPERTY_SAMPLE_RATE:
+                    // we don't have these in our database, so return 0
+                    list.append(id, property.code, MtpConstants.TYPE_UINT32, 0);
+                    break;
+                case MtpConstants.PROPERTY_BITRATE_TYPE:
+                case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
+                    // we don't have these in our database, so return 0
+                    list.append(id, property.code, MtpConstants.TYPE_UINT16, 0);
+                    break;
+                default:
+                    switch(property.type) {
+                        case MtpConstants.TYPE_UNDEFINED:
+                            list.append(id, property.code, property.type, 0);
                             break;
-                        case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
-                            // special case - need to extract file name from full path
-                            String value = c.getString(column);
-                            if (value != null) {
-                                result.append(handle, propertyCode, nameFromPath(value));
-                            } else {
-                                result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                            }
-                            break;
-                        case MtpConstants.PROPERTY_NAME:
-                            // first try title
-                            String name = c.getString(column);
-                            // then try name
-                            if (name == null) {
-                                name = queryString(handle, Audio.PlaylistsColumns.NAME);
-                            }
-                            // if title and name fail, extract name from full path
-                            if (name == null) {
-                                name = queryString(handle, Files.FileColumns.DATA);
-                                if (name != null) {
-                                    name = nameFromPath(name);
-                                }
-                            }
-                            if (name != null) {
-                                result.append(handle, propertyCode, name);
-                            } else {
-                                result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                            }
-                            break;
-                        case MtpConstants.PROPERTY_DATE_MODIFIED:
-                        case MtpConstants.PROPERTY_DATE_ADDED:
-                            // convert from seconds to DateTime
-                            result.append(handle, propertyCode, format_date_time(c.getInt(column)));
-                            break;
-                        case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
-                            // release date is stored internally as just the year
-                            int year = c.getInt(column);
-                            String dateTime = Integer.toString(year) + "0101T000000";
-                            result.append(handle, propertyCode, dateTime);
-                            break;
-                        case MtpConstants.PROPERTY_PERSISTENT_UID:
-                            // PUID is concatenation of storageID and object handle
-                            long puid = c.getLong(column);
-                            puid <<= 32;
-                            puid += handle;
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid);
-                            break;
-                        case MtpConstants.PROPERTY_TRACK:
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT16,
-                                        c.getInt(column) % 1000);
-                            break;
-                        case MtpConstants.PROPERTY_ARTIST:
-                            result.append(handle, propertyCode,
-                                    queryAudio(handle, Audio.AudioColumns.ARTIST));
-                            break;
-                        case MtpConstants.PROPERTY_ALBUM_NAME:
-                            result.append(handle, propertyCode,
-                                    queryAudio(handle, Audio.AudioColumns.ALBUM));
-                            break;
-                        case MtpConstants.PROPERTY_GENRE:
-                            String genre = queryGenre(handle);
-                            if (genre != null) {
-                                result.append(handle, propertyCode, genre);
-                            } else {
-                                result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                            }
-                            break;
-                        case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
-                        case MtpConstants.PROPERTY_AUDIO_BITRATE:
-                        case MtpConstants.PROPERTY_SAMPLE_RATE:
-                            // we don't have these in our database, so return 0
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT32, 0);
-                            break;
-                        case MtpConstants.PROPERTY_BITRATE_TYPE:
-                        case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
-                            // we don't have these in our database, so return 0
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
+                        case MtpConstants.TYPE_STR:
+                            String value = "";
+                            if (c != null)
+                                value = c.getString(property.column);
+                            list.append(id, property.code, value);
                             break;
                         default:
-                            if (property.type == MtpConstants.TYPE_STR) {
-                                result.append(handle, propertyCode, c.getString(column));
-                            } else if (property.type == MtpConstants.TYPE_UNDEFINED) {
-                                result.append(handle, propertyCode, property.type, 0);
-                            } else {
-                                result.append(handle, propertyCode, property.type,
-                                        c.getLong(column));
-                            }
-                            break;
+                            long longValue = 0L;
+                            if (c != null)
+                                longValue = c.getLong(property.column);
+                            list.append(id, property.code, property.type, longValue);
                     }
-                }
-            }
-
-            return result;
-        } catch (RemoteException e) {
-            return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
-        } finally {
-            if (c != null) {
-                c.close();
             }
         }
-        // impossible to get here, so no return statement
+        if (c != null)
+            c.close();
+        return MtpConstants.RESPONSE_OK;
     }
 
     private native String format_date_time(long seconds);
diff --git a/media/java/android/mtp/MtpPropertyList.java b/media/java/android/mtp/MtpPropertyList.java
index f9bc603..ede90da 100644
--- a/media/java/android/mtp/MtpPropertyList.java
+++ b/media/java/android/mtp/MtpPropertyList.java
@@ -16,6 +16,9 @@
 
 package android.mtp;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Encapsulates the ObjectPropList dataset used by the GetObjectPropList command.
  * The fields of this class are read by JNI code in android_media_MtpDatabase.cpp
@@ -23,56 +26,70 @@
 
 class MtpPropertyList {
 
-    // number of results returned
-    private int             mCount;
-    // maximum number of results
-    private final int       mMaxCount;
-    // result code for GetObjectPropList
-    public int              mResult;
     // list of object handles (first field in quadruplet)
-    public final int[]      mObjectHandles;
-    // list of object propery codes (second field in quadruplet)
-    public final int[]      mPropertyCodes;
+    private List<Integer> mObjectHandles;
+    // list of object property codes (second field in quadruplet)
+    private List<Integer> mPropertyCodes;
     // list of data type codes (third field in quadruplet)
-    public final int[]     mDataTypes;
+    private List<Integer> mDataTypes;
     // list of long int property values (fourth field in quadruplet, when value is integer type)
-    public long[]     mLongValues;
+    private List<Long> mLongValues;
     // list of long int property values (fourth field in quadruplet, when value is string type)
-    public String[]   mStringValues;
+    private List<String> mStringValues;
 
-    // constructor only called from MtpDatabase
-    public MtpPropertyList(int maxCount, int result) {
-        mMaxCount = maxCount;
-        mResult = result;
-        mObjectHandles = new int[maxCount];
-        mPropertyCodes = new int[maxCount];
-        mDataTypes = new int[maxCount];
-        // mLongValues and mStringValues are created lazily since both might not be necessary
+    // Return value of this operation
+    private int mCode;
+
+    public MtpPropertyList(int code) {
+        mCode = code;
+        mObjectHandles = new ArrayList<>();
+        mPropertyCodes = new ArrayList<>();
+        mDataTypes = new ArrayList<>();
+        mLongValues = new ArrayList<>();
+        mStringValues = new ArrayList<>();
     }
 
     public void append(int handle, int property, int type, long value) {
-        int index = mCount++;
-        if (mLongValues == null) {
-            mLongValues = new long[mMaxCount];
-        }
-        mObjectHandles[index] = handle;
-        mPropertyCodes[index] = property;
-        mDataTypes[index] = type;
-        mLongValues[index] = value;
+        mObjectHandles.add(handle);
+        mPropertyCodes.add(property);
+        mDataTypes.add(type);
+        mLongValues.add(value);
+        mStringValues.add(null);
     }
 
     public void append(int handle, int property, String value) {
-        int index = mCount++;
-        if (mStringValues == null) {
-            mStringValues = new String[mMaxCount];
-        }
-        mObjectHandles[index] = handle;
-        mPropertyCodes[index] = property;
-        mDataTypes[index] = MtpConstants.TYPE_STR;
-        mStringValues[index] = value;
+        mObjectHandles.add(handle);
+        mPropertyCodes.add(property);
+        mDataTypes.add(MtpConstants.TYPE_STR);
+        mStringValues.add(value);
+        mLongValues.add(0L);
     }
 
-    public void setResult(int result) {
-        mResult = result;
+    public int getCode() {
+        return mCode;
+    }
+
+    public int getCount() {
+        return mObjectHandles.size();
+    }
+
+    public int[] getObjectHandles() {
+        return mObjectHandles.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    public int[] getPropertyCodes() {
+        return mPropertyCodes.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    public int[] getDataTypes() {
+        return mDataTypes.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    public long[] getLongValues() {
+        return mLongValues.stream().mapToLong(Long::longValue).toArray();
+    }
+
+    public String[] getStringValues() {
+        return mStringValues.toArray(new String[0]);
     }
 }
diff --git a/media/java/android/mtp/MtpStorage.java b/media/java/android/mtp/MtpStorage.java
index 6ca442c..c72b827 100644
--- a/media/java/android/mtp/MtpStorage.java
+++ b/media/java/android/mtp/MtpStorage.java
@@ -31,15 +31,13 @@
     private final int mStorageId;
     private final String mPath;
     private final String mDescription;
-    private final long mReserveSpace;
     private final boolean mRemovable;
     private final long mMaxFileSize;
 
-    public MtpStorage(StorageVolume volume, Context context) {
-        mStorageId = volume.getStorageId();
+    public MtpStorage(StorageVolume volume, int storageId) {
+        mStorageId = storageId;
         mPath = volume.getPath();
-        mDescription = volume.getDescription(context);
-        mReserveSpace = volume.getMtpReserveSpace() * 1024L * 1024L;
+        mDescription = volume.getDescription(null);
         mRemovable = volume.isRemovable();
         mMaxFileSize = volume.getMaxFileSize();
     }
@@ -72,16 +70,6 @@
     }
 
    /**
-     * Returns the amount of space to reserve on the storage file system.
-     * This can be set to a non-zero value to prevent MTP from filling up the entire storage.
-     *
-     * @return reserved space in bytes.
-     */
-    public final long getReserveSpace() {
-        return mReserveSpace;
-    }
-
-   /**
      * Returns true if the storage is removable.
      *
      * @return is removable
diff --git a/media/java/android/mtp/MtpStorageManager.java b/media/java/android/mtp/MtpStorageManager.java
new file mode 100644
index 0000000..bdc8741
--- /dev/null
+++ b/media/java/android/mtp/MtpStorageManager.java
@@ -0,0 +1,1210 @@
+/*
+ * 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.storage.StorageVolume;
+import android.util.Log;
+
+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.Iterator;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * 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);
+            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 {
+                    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 {
+        // 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, MtpObject parent, boolean isDir) {
+            mId = id;
+            mName = name;
+            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 : getPath().toFile().length();
+        }
+
+        public Path getPath() {
+            return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName);
+        }
+
+        public boolean isRoot() {
+            return mParent == null;
+        }
+
+        /** 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) {
+            mParent = parent;
+        }
+
+        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, 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);
+    }
+
+    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() {
+        Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(),
+                mObjects.values().stream());
+
+        Iterator<MtpObject> iter = objs.iterator();
+        while (iter.hasNext()) {
+            // Close all FileObservers.
+            MtpObject obj = iter.next();
+            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) {
+        int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1;
+        MtpObject root = new MtpObject(volume.getPath(), storageId, null, true);
+        MtpStorage storage = new MtpStorage(volume, storageId);
+        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 stream of matched objects, or null if error
+     */
+    public synchronized Stream<MtpObject> getObjects(int parent, int format, int storageId) {
+        boolean recursive = parent == 0;
+        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.
+                ArrayList<Stream<MtpObject>> streamList = new ArrayList<>();
+                for (MtpObject root : mRoots.values()) {
+                    streamList.add(getObjects(root, format, recursive));
+                }
+                return Stream.of(streamList).flatMap(Collection::stream).reduce(Stream::concat)
+                        .orElseGet(Stream::empty);
+            }
+        }
+        MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent);
+        if (obj == null)
+            return null;
+        return getObjects(obj, format, recursive);
+    }
+
+    private synchronized Stream<MtpObject> getObjects(MtpObject parent, int format, boolean rec) {
+        Collection<MtpObject> children = getChildren(parent);
+        if (children == null)
+            return null;
+        Stream<MtpObject> ret = Stream.of(children).flatMap(Collection::stream);
+
+        if (format != 0) {
+            ret = ret.filter(o -> o.getFormat() == format);
+        }
+        if (rec) {
+            // Get all objects recursively.
+            ArrayList<Stream<MtpObject>> streamList = new ArrayList<>();
+            streamList.add(ret);
+            for (MtpObject o : children) {
+                if (o.isDir())
+                    streamList.add(getObjects(o, format, true));
+            }
+            ret = Stream.of(streamList).filter(Objects::nonNull).flatMap(Collection::stream)
+                    .reduce(Stream::concat).orElseGet(Stream::empty);
+        }
+        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, 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);
+    }
+
+    /**
+     * 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() {
+        Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(),
+                mObjects.values().stream());
+        Iterator<MtpObject> iter = objs.iterator();
+        boolean ret = true;
+        while (iter.hasNext()) {
+            MtpObject obj = iter.next();
+            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 dummy.
+            // 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 dummy 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 dummy.
+            // 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;
+    }
+}
diff --git a/media/jni/android_mtp_MtpDatabase.cpp b/media/jni/android_mtp_MtpDatabase.cpp
index 4e8c72b..23ef84f6 100644
--- a/media/jni/android_mtp_MtpDatabase.cpp
+++ b/media/jni/android_mtp_MtpDatabase.cpp
@@ -19,7 +19,7 @@
 
 #include "android_media_Utils.h"
 #include "mtp.h"
-#include "MtpDatabase.h"
+#include "IMtpDatabase.h"
 #include "MtpDataPacket.h"
 #include "MtpObjectInfo.h"
 #include "MtpProperty.h"
@@ -55,7 +55,7 @@
 
 static jmethodID method_beginSendObject;
 static jmethodID method_endSendObject;
-static jmethodID method_doScanDirectory;
+static jmethodID method_rescanFile;
 static jmethodID method_getObjectList;
 static jmethodID method_getNumObjects;
 static jmethodID method_getSupportedPlaybackFormats;
@@ -68,35 +68,34 @@
 static jmethodID method_getObjectPropertyList;
 static jmethodID method_getObjectInfo;
 static jmethodID method_getObjectFilePath;
-static jmethodID method_deleteFile;
-static jmethodID method_moveObject;
+static jmethodID method_beginDeleteObject;
+static jmethodID method_endDeleteObject;
+static jmethodID method_beginMoveObject;
+static jmethodID method_endMoveObject;
+static jmethodID method_beginCopyObject;
+static jmethodID method_endCopyObject;
 static jmethodID method_getObjectReferences;
 static jmethodID method_setObjectReferences;
-static jmethodID method_sessionStarted;
-static jmethodID method_sessionEnded;
 
 static jfieldID field_context;
-static jfieldID field_batteryLevel;
-static jfieldID field_batteryScale;
-static jfieldID field_deviceType;
 
-// MtpPropertyList fields
-static jfieldID field_mCount;
-static jfieldID field_mResult;
-static jfieldID field_mObjectHandles;
-static jfieldID field_mPropertyCodes;
-static jfieldID field_mDataTypes;
-static jfieldID field_mLongValues;
-static jfieldID field_mStringValues;
+// MtpPropertyList methods
+static jmethodID method_getCode;
+static jmethodID method_getCount;
+static jmethodID method_getObjectHandles;
+static jmethodID method_getPropertyCodes;
+static jmethodID method_getDataTypes;
+static jmethodID method_getLongValues;
+static jmethodID method_getStringValues;
 
 
-MtpDatabase* getMtpDatabase(JNIEnv *env, jobject database) {
-    return (MtpDatabase *)env->GetLongField(database, field_context);
+IMtpDatabase* getMtpDatabase(JNIEnv *env, jobject database) {
+    return (IMtpDatabase *)env->GetLongField(database, field_context);
 }
 
 // ----------------------------------------------------------------------------
 
-class MyMtpDatabase : public MtpDatabase {
+class MtpDatabase : public IMtpDatabase {
 private:
     jobject         mDatabase;
     jintArray       mIntBuffer;
@@ -104,23 +103,20 @@
     jcharArray      mStringBuffer;
 
 public:
-                                    MyMtpDatabase(JNIEnv *env, jobject client);
-    virtual                         ~MyMtpDatabase();
+                                    MtpDatabase(JNIEnv *env, jobject client);
+    virtual                         ~MtpDatabase();
     void                            cleanup(JNIEnv *env);
 
     virtual MtpObjectHandle         beginSendObject(const char* path,
                                             MtpObjectFormat format,
                                             MtpObjectHandle parent,
-                                            MtpStorageID storage,
-                                            uint64_t size,
-                                            time_t modified);
+                                            MtpStorageID storage);
 
-    virtual void                    endSendObject(const char* path,
+    virtual void                    endSendObject(MtpObjectHandle handle, bool succeeded);
+
+    virtual void                    rescanFile(const char* path,
                                             MtpObjectHandle handle,
-                                            MtpObjectFormat format,
-                                            bool succeeded);
-
-    virtual void                    doScanDirectory(const char* path);
+                                            MtpObjectFormat format);
 
     virtual MtpObjectHandleList*    getObjectList(MtpStorageID storageID,
                                     MtpObjectFormat format,
@@ -167,7 +163,8 @@
                                             MtpString& outFilePath,
                                             int64_t& outFileLength,
                                             MtpObjectFormat& outFormat);
-    virtual MtpResponseCode         deleteFile(MtpObjectHandle handle);
+    virtual MtpResponseCode         beginDeleteObject(MtpObjectHandle handle);
+    virtual void                    endDeleteObject(MtpObjectHandle handle, bool succeeded);
 
     bool                            getObjectPropertyInfo(MtpObjectProperty property, int& type);
     bool                            getDevicePropertyInfo(MtpDeviceProperty property, int& type);
@@ -182,12 +179,17 @@
 
     virtual MtpProperty*            getDevicePropertyDesc(MtpDeviceProperty property);
 
-    virtual MtpResponseCode         moveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
-                                            MtpStorageID newStorage, MtpString& newPath);
+    virtual MtpResponseCode         beginMoveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+                                            MtpStorageID newStorage);
 
-    virtual void                    sessionStarted();
+    virtual void                    endMoveObject(MtpObjectHandle oldParent, MtpObjectHandle newParent,
+                                            MtpStorageID oldStorage, MtpStorageID newStorage,
+                                             MtpObjectHandle handle, bool succeeded);
 
-    virtual void                    sessionEnded();
+    virtual MtpResponseCode         beginCopyObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+                                            MtpStorageID newStorage);
+    virtual void                    endCopyObject(MtpObjectHandle handle, bool succeeded);
+
 };
 
 // ----------------------------------------------------------------------------
@@ -202,7 +204,7 @@
 
 // ----------------------------------------------------------------------------
 
-MyMtpDatabase::MyMtpDatabase(JNIEnv *env, jobject client)
+MtpDatabase::MtpDatabase(JNIEnv *env, jobject client)
     :   mDatabase(env->NewGlobalRef(client)),
         mIntBuffer(NULL),
         mLongBuffer(NULL),
@@ -228,27 +230,24 @@
     mStringBuffer = (jcharArray)env->NewGlobalRef(charArray);
 }
 
-void MyMtpDatabase::cleanup(JNIEnv *env) {
+void MtpDatabase::cleanup(JNIEnv *env) {
     env->DeleteGlobalRef(mDatabase);
     env->DeleteGlobalRef(mIntBuffer);
     env->DeleteGlobalRef(mLongBuffer);
     env->DeleteGlobalRef(mStringBuffer);
 }
 
-MyMtpDatabase::~MyMtpDatabase() {
+MtpDatabase::~MtpDatabase() {
 }
 
-MtpObjectHandle MyMtpDatabase::beginSendObject(const char* path,
+MtpObjectHandle MtpDatabase::beginSendObject(const char* path,
                                                MtpObjectFormat format,
                                                MtpObjectHandle parent,
-                                               MtpStorageID storage,
-                                               uint64_t size,
-                                               time_t modified) {
+                                               MtpStorageID storage) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jstring pathStr = env->NewStringUTF(path);
     MtpObjectHandle result = env->CallIntMethod(mDatabase, method_beginSendObject,
-            pathStr, (jint)format, (jint)parent, (jint)storage,
-            (jlong)size, (jlong)modified);
+            pathStr, (jint)format, (jint)parent, (jint)storage);
 
     if (pathStr)
         env->DeleteLocalRef(pathStr);
@@ -256,29 +255,26 @@
     return result;
 }
 
-void MyMtpDatabase::endSendObject(const char* path, MtpObjectHandle handle,
-                                  MtpObjectFormat format, bool succeeded) {
+void MtpDatabase::endSendObject(MtpObjectHandle handle, bool succeeded) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mDatabase, method_endSendObject, (jint)handle, (jboolean)succeeded);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+void MtpDatabase::rescanFile(const char* path, MtpObjectHandle handle,
+                                  MtpObjectFormat format) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jstring pathStr = env->NewStringUTF(path);
-    env->CallVoidMethod(mDatabase, method_endSendObject, pathStr,
-                        (jint)handle, (jint)format, (jboolean)succeeded);
+    env->CallVoidMethod(mDatabase, method_rescanFile, pathStr,
+                        (jint)handle, (jint)format);
 
     if (pathStr)
         env->DeleteLocalRef(pathStr);
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
 }
 
-void MyMtpDatabase::doScanDirectory(const char* path) {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jstring pathStr = env->NewStringUTF(path);
-    env->CallVoidMethod(mDatabase, method_doScanDirectory, pathStr);
-
-    if (pathStr)
-        env->DeleteLocalRef(pathStr);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-}
-
-MtpObjectHandleList* MyMtpDatabase::getObjectList(MtpStorageID storageID,
+MtpObjectHandleList* MtpDatabase::getObjectList(MtpStorageID storageID,
                                                   MtpObjectFormat format,
                                                   MtpObjectHandle parent) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -298,7 +294,7 @@
     return list;
 }
 
-int MyMtpDatabase::getNumObjects(MtpStorageID storageID,
+int MtpDatabase::getNumObjects(MtpStorageID storageID,
                                  MtpObjectFormat format,
                                  MtpObjectHandle parent) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -309,7 +305,7 @@
     return result;
 }
 
-MtpObjectFormatList* MyMtpDatabase::getSupportedPlaybackFormats() {
+MtpObjectFormatList* MtpDatabase::getSupportedPlaybackFormats() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedPlaybackFormats);
@@ -327,7 +323,7 @@
     return list;
 }
 
-MtpObjectFormatList* MyMtpDatabase::getSupportedCaptureFormats() {
+MtpObjectFormatList* MtpDatabase::getSupportedCaptureFormats() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedCaptureFormats);
@@ -345,7 +341,7 @@
     return list;
 }
 
-MtpObjectPropertyList* MyMtpDatabase::getSupportedObjectProperties(MtpObjectFormat format) {
+MtpObjectPropertyList* MtpDatabase::getSupportedObjectProperties(MtpObjectFormat format) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedObjectProperties, (jint)format);
@@ -363,7 +359,7 @@
     return list;
 }
 
-MtpDevicePropertyList* MyMtpDatabase::getSupportedDeviceProperties() {
+MtpDevicePropertyList* MtpDatabase::getSupportedDeviceProperties() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedDeviceProperties);
@@ -381,7 +377,7 @@
     return list;
 }
 
-MtpResponseCode MyMtpDatabase::getObjectPropertyValue(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectPropertyValue(MtpObjectHandle handle,
                                                       MtpObjectProperty property,
                                                       MtpDataPacket& packet) {
     static_assert(sizeof(jint) >= sizeof(MtpObjectHandle),
@@ -397,42 +393,26 @@
             static_cast<jint>(property),
             0,
             0);
-    MtpResponseCode result = env->GetIntField(list, field_mResult);
-    int count = env->GetIntField(list, field_mCount);
-    if (result == MTP_RESPONSE_OK && count != 1)
+    MtpResponseCode result = env->CallIntMethod(list, method_getCode);
+    jint count = env->CallIntMethod(list, method_getCount);
+    if (count != 1)
         result = MTP_RESPONSE_GENERAL_ERROR;
 
     if (result == MTP_RESPONSE_OK) {
-        jintArray objectHandlesArray = (jintArray)env->GetObjectField(list, field_mObjectHandles);
-        jintArray propertyCodesArray = (jintArray)env->GetObjectField(list, field_mPropertyCodes);
-        jintArray dataTypesArray = (jintArray)env->GetObjectField(list, field_mDataTypes);
-        jlongArray longValuesArray = (jlongArray)env->GetObjectField(list, field_mLongValues);
-        jobjectArray stringValuesArray = (jobjectArray)env->GetObjectField(list, field_mStringValues);
+        jintArray objectHandlesArray = (jintArray)env->CallObjectMethod(list, method_getObjectHandles);
+        jintArray propertyCodesArray = (jintArray)env->CallObjectMethod(list, method_getPropertyCodes);
+        jintArray dataTypesArray = (jintArray)env->CallObjectMethod(list, method_getDataTypes);
+        jlongArray longValuesArray = (jlongArray)env->CallObjectMethod(list, method_getLongValues);
+        jobjectArray stringValuesArray = (jobjectArray)env->CallObjectMethod(list, method_getStringValues);
 
         jint* objectHandles = env->GetIntArrayElements(objectHandlesArray, 0);
         jint* propertyCodes = env->GetIntArrayElements(propertyCodesArray, 0);
         jint* dataTypes = env->GetIntArrayElements(dataTypesArray, 0);
-        jlong* longValues = (longValuesArray ? env->GetLongArrayElements(longValuesArray, 0) : NULL);
+        jlong* longValues = env->GetLongArrayElements(longValuesArray, 0);
 
         int type = dataTypes[0];
         jlong longValue = (longValues ? longValues[0] : 0);
 
-        // special case date properties, which are strings to MTP
-        // but stored internally as a uint64
-        if (property == MTP_PROPERTY_DATE_MODIFIED || property == MTP_PROPERTY_DATE_ADDED) {
-            char    date[20];
-            formatDateTime(longValue, date, sizeof(date));
-            packet.putString(date);
-            goto out;
-        }
-        // release date is stored internally as just the year
-        if (property == MTP_PROPERTY_ORIGINAL_RELEASE_DATE) {
-            char    date[20];
-            snprintf(date, sizeof(date), "%04" PRId64 "0101T000000", longValue);
-            packet.putString(date);
-            goto out;
-        }
-
         switch (type) {
             case MTP_TYPE_INT8:
                 packet.putInt8(longValue);
@@ -481,20 +461,16 @@
                 ALOGE("unsupported type in getObjectPropertyValue\n");
                 result = MTP_RESPONSE_INVALID_OBJECT_PROP_FORMAT;
         }
-out:
         env->ReleaseIntArrayElements(objectHandlesArray, objectHandles, 0);
         env->ReleaseIntArrayElements(propertyCodesArray, propertyCodes, 0);
         env->ReleaseIntArrayElements(dataTypesArray, dataTypes, 0);
-        if (longValues)
-            env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
+        env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
 
         env->DeleteLocalRef(objectHandlesArray);
         env->DeleteLocalRef(propertyCodesArray);
         env->DeleteLocalRef(dataTypesArray);
-        if (longValuesArray)
-            env->DeleteLocalRef(longValuesArray);
-        if (stringValuesArray)
-            env->DeleteLocalRef(stringValuesArray);
+        env->DeleteLocalRef(longValuesArray);
+        env->DeleteLocalRef(stringValuesArray);
     }
 
     env->DeleteLocalRef(list);
@@ -559,7 +535,7 @@
     return true;
 }
 
-MtpResponseCode MyMtpDatabase::setObjectPropertyValue(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::setObjectPropertyValue(MtpObjectHandle handle,
                                                       MtpObjectProperty property,
                                                       MtpDataPacket& packet) {
     int         type;
@@ -590,80 +566,73 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::getDevicePropertyValue(MtpDeviceProperty property,
+MtpResponseCode MtpDatabase::getDevicePropertyValue(MtpDeviceProperty property,
                                                       MtpDataPacket& packet) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
+    int type;
 
-    if (property == MTP_DEVICE_PROPERTY_BATTERY_LEVEL) {
-        // special case - implemented here instead of Java
-        packet.putUInt8((uint8_t)env->GetIntField(mDatabase, field_batteryLevel));
-        return MTP_RESPONSE_OK;
-    } else {
-        int type;
+    if (!getDevicePropertyInfo(property, type))
+        return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
 
-        if (!getDevicePropertyInfo(property, type))
-            return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
-
-        jint result = env->CallIntMethod(mDatabase, method_getDeviceProperty,
-                    (jint)property, mLongBuffer, mStringBuffer);
-        if (result != MTP_RESPONSE_OK) {
-            checkAndClearExceptionFromCallback(env, __FUNCTION__);
-            return result;
-        }
-
-        jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
-        jlong longValue = longValues[0];
-        env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
-
-        switch (type) {
-            case MTP_TYPE_INT8:
-                packet.putInt8(longValue);
-                break;
-            case MTP_TYPE_UINT8:
-                packet.putUInt8(longValue);
-                break;
-            case MTP_TYPE_INT16:
-                packet.putInt16(longValue);
-                break;
-            case MTP_TYPE_UINT16:
-                packet.putUInt16(longValue);
-                break;
-            case MTP_TYPE_INT32:
-                packet.putInt32(longValue);
-                break;
-            case MTP_TYPE_UINT32:
-                packet.putUInt32(longValue);
-                break;
-            case MTP_TYPE_INT64:
-                packet.putInt64(longValue);
-                break;
-            case MTP_TYPE_UINT64:
-                packet.putUInt64(longValue);
-                break;
-            case MTP_TYPE_INT128:
-                packet.putInt128(longValue);
-                break;
-            case MTP_TYPE_UINT128:
-                packet.putInt128(longValue);
-                break;
-            case MTP_TYPE_STR:
-            {
-                jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
-                packet.putString(str);
-                env->ReleaseCharArrayElements(mStringBuffer, str, 0);
-                break;
-             }
-            default:
-                ALOGE("unsupported type in getDevicePropertyValue\n");
-                return MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT;
-        }
-
+    jint result = env->CallIntMethod(mDatabase, method_getDeviceProperty,
+                (jint)property, mLongBuffer, mStringBuffer);
+    if (result != MTP_RESPONSE_OK) {
         checkAndClearExceptionFromCallback(env, __FUNCTION__);
-        return MTP_RESPONSE_OK;
+        return result;
     }
+
+    jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
+    jlong longValue = longValues[0];
+    env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
+
+    switch (type) {
+        case MTP_TYPE_INT8:
+            packet.putInt8(longValue);
+            break;
+        case MTP_TYPE_UINT8:
+            packet.putUInt8(longValue);
+            break;
+        case MTP_TYPE_INT16:
+            packet.putInt16(longValue);
+            break;
+        case MTP_TYPE_UINT16:
+            packet.putUInt16(longValue);
+            break;
+        case MTP_TYPE_INT32:
+            packet.putInt32(longValue);
+            break;
+        case MTP_TYPE_UINT32:
+            packet.putUInt32(longValue);
+            break;
+        case MTP_TYPE_INT64:
+            packet.putInt64(longValue);
+            break;
+        case MTP_TYPE_UINT64:
+            packet.putUInt64(longValue);
+            break;
+        case MTP_TYPE_INT128:
+            packet.putInt128(longValue);
+            break;
+        case MTP_TYPE_UINT128:
+            packet.putInt128(longValue);
+            break;
+        case MTP_TYPE_STR:
+        {
+            jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
+            packet.putString(str);
+            env->ReleaseCharArrayElements(mStringBuffer, str, 0);
+            break;
+        }
+        default:
+            ALOGE("unsupported type in getDevicePropertyValue\n");
+            return MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT;
+    }
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+    return MTP_RESPONSE_OK;
 }
 
-MtpResponseCode MyMtpDatabase::setDevicePropertyValue(MtpDeviceProperty property,
+MtpResponseCode MtpDatabase::setDevicePropertyValue(MtpDeviceProperty property,
                                                       MtpDataPacket& packet) {
     int         type;
 
@@ -693,11 +662,11 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::resetDeviceProperty(MtpDeviceProperty /*property*/) {
+MtpResponseCode MtpDatabase::resetDeviceProperty(MtpDeviceProperty /*property*/) {
     return -1;
 }
 
-MtpResponseCode MyMtpDatabase::getObjectPropertyList(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectPropertyList(MtpObjectHandle handle,
                                                      uint32_t format, uint32_t property,
                                                      int groupCode, int depth,
                                                      MtpDataPacket& packet) {
@@ -715,16 +684,16 @@
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     if (!list)
         return MTP_RESPONSE_GENERAL_ERROR;
-    int count = env->GetIntField(list, field_mCount);
-    MtpResponseCode result = env->GetIntField(list, field_mResult);
+    int count = env->CallIntMethod(list, method_getCount);
+    MtpResponseCode result = env->CallIntMethod(list, method_getCode);
 
     packet.putUInt32(count);
     if (count > 0) {
-        jintArray objectHandlesArray = (jintArray)env->GetObjectField(list, field_mObjectHandles);
-        jintArray propertyCodesArray = (jintArray)env->GetObjectField(list, field_mPropertyCodes);
-        jintArray dataTypesArray = (jintArray)env->GetObjectField(list, field_mDataTypes);
-        jlongArray longValuesArray = (jlongArray)env->GetObjectField(list, field_mLongValues);
-        jobjectArray stringValuesArray = (jobjectArray)env->GetObjectField(list, field_mStringValues);
+        jintArray objectHandlesArray = (jintArray)env->CallObjectMethod(list, method_getObjectHandles);
+        jintArray propertyCodesArray = (jintArray)env->CallObjectMethod(list, method_getPropertyCodes);
+        jintArray dataTypesArray = (jintArray)env->CallObjectMethod(list, method_getDataTypes);
+        jlongArray longValuesArray = (jlongArray)env->CallObjectMethod(list, method_getLongValues);
+        jobjectArray stringValuesArray = (jobjectArray)env->CallObjectMethod(list, method_getStringValues);
 
         jint* objectHandles = env->GetIntArrayElements(objectHandlesArray, 0);
         jint* propertyCodes = env->GetIntArrayElements(propertyCodesArray, 0);
@@ -781,7 +750,7 @@
                     break;
                 }
                 default:
-                    ALOGE("bad or unsupported data type in MyMtpDatabase::getObjectPropertyList");
+                    ALOGE("bad or unsupported data type in MtpDatabase::getObjectPropertyList");
                     break;
             }
         }
@@ -789,16 +758,13 @@
         env->ReleaseIntArrayElements(objectHandlesArray, objectHandles, 0);
         env->ReleaseIntArrayElements(propertyCodesArray, propertyCodes, 0);
         env->ReleaseIntArrayElements(dataTypesArray, dataTypes, 0);
-        if (longValues)
-            env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
+        env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
 
         env->DeleteLocalRef(objectHandlesArray);
         env->DeleteLocalRef(propertyCodesArray);
         env->DeleteLocalRef(dataTypesArray);
-        if (longValuesArray)
-            env->DeleteLocalRef(longValuesArray);
-        if (stringValuesArray)
-            env->DeleteLocalRef(stringValuesArray);
+        env->DeleteLocalRef(longValuesArray);
+        env->DeleteLocalRef(stringValuesArray);
     }
 
     env->DeleteLocalRef(list);
@@ -822,7 +788,7 @@
     return exif_get_long(e->data, o);
 }
 
-MtpResponseCode MyMtpDatabase::getObjectInfo(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectInfo(MtpObjectHandle handle,
                                              MtpObjectInfo& info) {
     MtpString       path;
     int64_t         length;
@@ -914,7 +880,7 @@
     return MTP_RESPONSE_OK;
 }
 
-void* MyMtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) {
+void* MtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) {
     MtpString path;
     int64_t length;
     MtpObjectFormat format;
@@ -979,7 +945,7 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::getObjectFilePath(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectFilePath(MtpObjectHandle handle,
                                                  MtpString& outFilePath,
                                                  int64_t& outFileLength,
                                                  MtpObjectFormat& outFormat) {
@@ -1005,26 +971,60 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::deleteFile(MtpObjectHandle handle) {
+MtpResponseCode MtpDatabase::beginDeleteObject(MtpObjectHandle handle) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
-    MtpResponseCode result = env->CallIntMethod(mDatabase, method_deleteFile, (jint)handle);
+    MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginDeleteObject, (jint)handle);
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::moveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
-        MtpStorageID newStorage, MtpString &newPath) {
+void MtpDatabase::endDeleteObject(MtpObjectHandle handle, bool succeeded) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jstring stringValue = env->NewStringUTF((const char *) newPath);
-    MtpResponseCode result = env->CallIntMethod(mDatabase, method_moveObject,
-                (jint)handle, (jint)newParent, (jint) newStorage, stringValue);
+    env->CallVoidMethod(mDatabase, method_endDeleteObject, (jint)handle, (jboolean) succeeded);
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
-    env->DeleteLocalRef(stringValue);
+}
+
+MtpResponseCode MtpDatabase::beginMoveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+        MtpStorageID newStorage) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginMoveObject,
+                (jint)handle, (jint)newParent, (jint) newStorage);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return result;
 }
 
+void MtpDatabase::endMoveObject(MtpObjectHandle oldParent, MtpObjectHandle newParent,
+                                            MtpStorageID oldStorage, MtpStorageID newStorage,
+                                             MtpObjectHandle handle, bool succeeded) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mDatabase, method_endMoveObject,
+                (jint)oldParent, (jint) newParent, (jint) oldStorage, (jint) newStorage,
+                (jint) handle, (jboolean) succeeded);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+MtpResponseCode MtpDatabase::beginCopyObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+        MtpStorageID newStorage) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginCopyObject,
+                (jint)handle, (jint)newParent, (jint) newStorage);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+    return result;
+}
+
+void MtpDatabase::endCopyObject(MtpObjectHandle handle, bool succeeded) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mDatabase, method_endCopyObject, (jint)handle, (jboolean)succeeded);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+
 struct PropertyTableEntry {
     MtpObjectProperty   property;
     int                 type;
@@ -1066,7 +1066,7 @@
     {   MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,      MTP_TYPE_UINT32 },
 };
 
-bool MyMtpDatabase::getObjectPropertyInfo(MtpObjectProperty property, int& type) {
+bool MtpDatabase::getObjectPropertyInfo(MtpObjectProperty property, int& type) {
     int count = sizeof(kObjectPropertyTable) / sizeof(kObjectPropertyTable[0]);
     const PropertyTableEntry* entry = kObjectPropertyTable;
     for (int i = 0; i < count; i++, entry++) {
@@ -1078,7 +1078,7 @@
     return false;
 }
 
-bool MyMtpDatabase::getDevicePropertyInfo(MtpDeviceProperty property, int& type) {
+bool MtpDatabase::getDevicePropertyInfo(MtpDeviceProperty property, int& type) {
     int count = sizeof(kDevicePropertyTable) / sizeof(kDevicePropertyTable[0]);
     const PropertyTableEntry* entry = kDevicePropertyTable;
     for (int i = 0; i < count; i++, entry++) {
@@ -1090,7 +1090,7 @@
     return false;
 }
 
-MtpObjectHandleList* MyMtpDatabase::getObjectReferences(MtpObjectHandle handle) {
+MtpObjectHandleList* MtpDatabase::getObjectReferences(MtpObjectHandle handle) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase, method_getObjectReferences,
                 (jint)handle);
@@ -1108,7 +1108,7 @@
     return list;
 }
 
-MtpResponseCode MyMtpDatabase::setObjectReferences(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::setObjectReferences(MtpObjectHandle handle,
                                                    MtpObjectHandleList* references) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     int count = references->size();
@@ -1129,7 +1129,7 @@
     return result;
 }
 
-MtpProperty* MyMtpDatabase::getObjectPropertyDesc(MtpObjectProperty property,
+MtpProperty* MtpDatabase::getObjectPropertyDesc(MtpObjectProperty property,
                                                   MtpObjectFormat format) {
     static const int channelEnum[] = {
                                         1,  // mono
@@ -1210,67 +1210,65 @@
     return result;
 }
 
-MtpProperty* MyMtpDatabase::getDevicePropertyDesc(MtpDeviceProperty property) {
+MtpProperty* MtpDatabase::getDevicePropertyDesc(MtpDeviceProperty property) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     MtpProperty* result = NULL;
     bool writable = false;
 
-    switch (property) {
-        case MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
-        case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
-            writable = true;
-            // fall through
-        case MTP_DEVICE_PROPERTY_IMAGE_SIZE: {
-            result = new MtpProperty(property, MTP_TYPE_STR, writable);
-
-            // get current value
-            jint ret = env->CallIntMethod(mDatabase, method_getDeviceProperty,
-                        (jint)property, mLongBuffer, mStringBuffer);
-            if (ret == MTP_RESPONSE_OK) {
+    // get current value
+    jint ret = env->CallIntMethod(mDatabase, method_getDeviceProperty,
+        (jint)property, mLongBuffer, mStringBuffer);
+    if (ret == MTP_RESPONSE_OK) {
+        switch (property) {
+            case MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
+            case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
+                writable = true;
+                // fall through
+            case MTP_DEVICE_PROPERTY_IMAGE_SIZE:
+            {
+                result = new MtpProperty(property, MTP_TYPE_STR, writable);
                 jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
                 result->setCurrentValue(str);
                 // for read-only properties it is safe to assume current value is default value
                 if (!writable)
                     result->setDefaultValue(str);
                 env->ReleaseCharArrayElements(mStringBuffer, str, 0);
-            } else {
-                ALOGE("unable to read device property, response: %04X", ret);
+                break;
             }
-            break;
+            case MTP_DEVICE_PROPERTY_BATTERY_LEVEL:
+            {
+                result = new MtpProperty(property, MTP_TYPE_UINT8);
+                jlong* arr = env->GetLongArrayElements(mLongBuffer, 0);
+                result->setFormRange(0, arr[1], 1);
+                result->mCurrentValue.u.u8 = (uint8_t) arr[0];
+                env->ReleaseLongArrayElements(mLongBuffer, arr, 0);
+                break;
+            }
+            case MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
+            {
+                jlong* arr = env->GetLongArrayElements(mLongBuffer, 0);
+                result = new MtpProperty(property, MTP_TYPE_UINT32);
+                result->mCurrentValue.u.u32 = (uint32_t) arr[0];
+                env->ReleaseLongArrayElements(mLongBuffer, arr, 0);
+                break;
+            }
+            default:
+                ALOGE("Unrecognized property %x", property);
         }
-        case MTP_DEVICE_PROPERTY_BATTERY_LEVEL:
-            result = new MtpProperty(property, MTP_TYPE_UINT8);
-            result->setFormRange(0, env->GetIntField(mDatabase, field_batteryScale), 1);
-            result->mCurrentValue.u.u8 = (uint8_t)env->GetIntField(mDatabase, field_batteryLevel);
-            break;
-        case MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
-            result = new MtpProperty(property, MTP_TYPE_UINT32);
-            result->mCurrentValue.u.u32 = (uint32_t)env->GetIntField(mDatabase, field_deviceType);
-            break;
+    } else {
+        ALOGE("unable to read device property, response: %04X", ret);
     }
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return result;
 }
 
-void MyMtpDatabase::sessionStarted() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->CallVoidMethod(mDatabase, method_sessionStarted);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-}
-
-void MyMtpDatabase::sessionEnded() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->CallVoidMethod(mDatabase, method_sessionEnded);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-}
-
 // ----------------------------------------------------------------------------
 
 static void
 android_mtp_MtpDatabase_setup(JNIEnv *env, jobject thiz)
 {
-    MyMtpDatabase* database = new MyMtpDatabase(env, thiz);
+    MtpDatabase* database = new MtpDatabase(env, thiz);
     env->SetLongField(thiz, field_context, (jlong)database);
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
 }
@@ -1278,7 +1276,7 @@
 static void
 android_mtp_MtpDatabase_finalize(JNIEnv *env, jobject thiz)
 {
-    MyMtpDatabase* database = (MyMtpDatabase *)env->GetLongField(thiz, field_context);
+    MtpDatabase* database = (MtpDatabase *)env->GetLongField(thiz, field_context);
     database->cleanup(env);
     delete database;
     env->SetLongField(thiz, field_context, 0);
@@ -1305,6 +1303,13 @@
                                         (void *)android_mtp_MtpPropertyGroup_format_date_time},
 };
 
+#define GET_METHOD_ID(name, jclass, signature)                              \
+    method_##name = env->GetMethodID(jclass, #name, signature);             \
+    if (method_##name == NULL) {                                            \
+        ALOGE("Can't find " #name);                                         \
+        return -1;                                                          \
+    }                                                                       \
+
 int register_android_mtp_MtpDatabase(JNIEnv *env)
 {
     jclass clazz;
@@ -1314,175 +1319,48 @@
         ALOGE("Can't find android/mtp/MtpDatabase");
         return -1;
     }
-    method_beginSendObject = env->GetMethodID(clazz, "beginSendObject", "(Ljava/lang/String;IIIJJ)I");
-    if (method_beginSendObject == NULL) {
-        ALOGE("Can't find beginSendObject");
-        return -1;
-    }
-    method_endSendObject = env->GetMethodID(clazz, "endSendObject", "(Ljava/lang/String;IIZ)V");
-    if (method_endSendObject == NULL) {
-        ALOGE("Can't find endSendObject");
-        return -1;
-    }
-    method_doScanDirectory = env->GetMethodID(clazz, "doScanDirectory", "(Ljava/lang/String;)V");
-    if (method_doScanDirectory == NULL) {
-        ALOGE("Can't find doScanDirectory");
-        return -1;
-    }
-    method_getObjectList = env->GetMethodID(clazz, "getObjectList", "(III)[I");
-    if (method_getObjectList == NULL) {
-        ALOGE("Can't find getObjectList");
-        return -1;
-    }
-    method_getNumObjects = env->GetMethodID(clazz, "getNumObjects", "(III)I");
-    if (method_getNumObjects == NULL) {
-        ALOGE("Can't find getNumObjects");
-        return -1;
-    }
-    method_getSupportedPlaybackFormats = env->GetMethodID(clazz, "getSupportedPlaybackFormats", "()[I");
-    if (method_getSupportedPlaybackFormats == NULL) {
-        ALOGE("Can't find getSupportedPlaybackFormats");
-        return -1;
-    }
-    method_getSupportedCaptureFormats = env->GetMethodID(clazz, "getSupportedCaptureFormats", "()[I");
-    if (method_getSupportedCaptureFormats == NULL) {
-        ALOGE("Can't find getSupportedCaptureFormats");
-        return -1;
-    }
-    method_getSupportedObjectProperties = env->GetMethodID(clazz, "getSupportedObjectProperties", "(I)[I");
-    if (method_getSupportedObjectProperties == NULL) {
-        ALOGE("Can't find getSupportedObjectProperties");
-        return -1;
-    }
-    method_getSupportedDeviceProperties = env->GetMethodID(clazz, "getSupportedDeviceProperties", "()[I");
-    if (method_getSupportedDeviceProperties == NULL) {
-        ALOGE("Can't find getSupportedDeviceProperties");
-        return -1;
-    }
-    method_setObjectProperty = env->GetMethodID(clazz, "setObjectProperty", "(IIJLjava/lang/String;)I");
-    if (method_setObjectProperty == NULL) {
-        ALOGE("Can't find setObjectProperty");
-        return -1;
-    }
-    method_getDeviceProperty = env->GetMethodID(clazz, "getDeviceProperty", "(I[J[C)I");
-    if (method_getDeviceProperty == NULL) {
-        ALOGE("Can't find getDeviceProperty");
-        return -1;
-    }
-    method_setDeviceProperty = env->GetMethodID(clazz, "setDeviceProperty", "(IJLjava/lang/String;)I");
-    if (method_setDeviceProperty == NULL) {
-        ALOGE("Can't find setDeviceProperty");
-        return -1;
-    }
-    method_getObjectPropertyList = env->GetMethodID(clazz, "getObjectPropertyList",
-            "(IIIII)Landroid/mtp/MtpPropertyList;");
-    if (method_getObjectPropertyList == NULL) {
-        ALOGE("Can't find getObjectPropertyList");
-        return -1;
-    }
-    method_getObjectInfo = env->GetMethodID(clazz, "getObjectInfo", "(I[I[C[J)Z");
-    if (method_getObjectInfo == NULL) {
-        ALOGE("Can't find getObjectInfo");
-        return -1;
-    }
-    method_getObjectFilePath = env->GetMethodID(clazz, "getObjectFilePath", "(I[C[J)I");
-    if (method_getObjectFilePath == NULL) {
-        ALOGE("Can't find getObjectFilePath");
-        return -1;
-    }
-    method_deleteFile = env->GetMethodID(clazz, "deleteFile", "(I)I");
-    if (method_deleteFile == NULL) {
-        ALOGE("Can't find deleteFile");
-        return -1;
-    }
-    method_moveObject = env->GetMethodID(clazz, "moveObject", "(IIILjava/lang/String;)I");
-    if (method_moveObject == NULL) {
-        ALOGE("Can't find moveObject");
-        return -1;
-    }
-    method_getObjectReferences = env->GetMethodID(clazz, "getObjectReferences", "(I)[I");
-    if (method_getObjectReferences == NULL) {
-        ALOGE("Can't find getObjectReferences");
-        return -1;
-    }
-    method_setObjectReferences = env->GetMethodID(clazz, "setObjectReferences", "(I[I)I");
-    if (method_setObjectReferences == NULL) {
-        ALOGE("Can't find setObjectReferences");
-        return -1;
-    }
-    method_sessionStarted = env->GetMethodID(clazz, "sessionStarted", "()V");
-    if (method_sessionStarted == NULL) {
-        ALOGE("Can't find sessionStarted");
-        return -1;
-    }
-    method_sessionEnded = env->GetMethodID(clazz, "sessionEnded", "()V");
-    if (method_sessionEnded == NULL) {
-        ALOGE("Can't find sessionEnded");
-        return -1;
-    }
+    GET_METHOD_ID(beginSendObject, clazz, "(Ljava/lang/String;III)I");
+    GET_METHOD_ID(endSendObject, clazz, "(IZ)V");
+    GET_METHOD_ID(rescanFile, clazz, "(Ljava/lang/String;II)V");
+    GET_METHOD_ID(getObjectList, clazz, "(III)[I");
+    GET_METHOD_ID(getNumObjects, clazz, "(III)I");
+    GET_METHOD_ID(getSupportedPlaybackFormats, clazz, "()[I");
+    GET_METHOD_ID(getSupportedCaptureFormats, clazz, "()[I");
+    GET_METHOD_ID(getSupportedObjectProperties, clazz, "(I)[I");
+    GET_METHOD_ID(getSupportedDeviceProperties, clazz, "()[I");
+    GET_METHOD_ID(setObjectProperty, clazz, "(IIJLjava/lang/String;)I");
+    GET_METHOD_ID(getDeviceProperty, clazz, "(I[J[C)I");
+    GET_METHOD_ID(setDeviceProperty, clazz, "(IJLjava/lang/String;)I");
+    GET_METHOD_ID(getObjectPropertyList, clazz, "(IIIII)Landroid/mtp/MtpPropertyList;");
+    GET_METHOD_ID(getObjectInfo, clazz, "(I[I[C[J)Z");
+    GET_METHOD_ID(getObjectFilePath, clazz, "(I[C[J)I");
+    GET_METHOD_ID(beginDeleteObject, clazz, "(I)I");
+    GET_METHOD_ID(endDeleteObject, clazz, "(IZ)V");
+    GET_METHOD_ID(beginMoveObject, clazz, "(III)I");
+    GET_METHOD_ID(endMoveObject, clazz, "(IIIIIZ)V");
+    GET_METHOD_ID(beginCopyObject, clazz, "(III)I");
+    GET_METHOD_ID(endCopyObject, clazz, "(IZ)V");
+    GET_METHOD_ID(getObjectReferences, clazz, "(I)[I");
+    GET_METHOD_ID(setObjectReferences, clazz, "(I[I)I");
 
     field_context = env->GetFieldID(clazz, "mNativeContext", "J");
     if (field_context == NULL) {
         ALOGE("Can't find MtpDatabase.mNativeContext");
         return -1;
     }
-    field_batteryLevel = env->GetFieldID(clazz, "mBatteryLevel", "I");
-    if (field_batteryLevel == NULL) {
-        ALOGE("Can't find MtpDatabase.mBatteryLevel");
-        return -1;
-    }
-    field_batteryScale = env->GetFieldID(clazz, "mBatteryScale", "I");
-    if (field_batteryScale == NULL) {
-        ALOGE("Can't find MtpDatabase.mBatteryScale");
-        return -1;
-    }
-    field_deviceType = env->GetFieldID(clazz, "mDeviceType", "I");
-    if (field_deviceType == NULL) {
-        ALOGE("Can't find MtpDatabase.mDeviceType");
-        return -1;
-    }
 
-    // now set up fields for MtpPropertyList class
     clazz = env->FindClass("android/mtp/MtpPropertyList");
     if (clazz == NULL) {
         ALOGE("Can't find android/mtp/MtpPropertyList");
         return -1;
     }
-    field_mCount = env->GetFieldID(clazz, "mCount", "I");
-    if (field_mCount == NULL) {
-        ALOGE("Can't find MtpPropertyList.mCount");
-        return -1;
-    }
-    field_mResult = env->GetFieldID(clazz, "mResult", "I");
-    if (field_mResult == NULL) {
-        ALOGE("Can't find MtpPropertyList.mResult");
-        return -1;
-    }
-    field_mObjectHandles = env->GetFieldID(clazz, "mObjectHandles", "[I");
-    if (field_mObjectHandles == NULL) {
-        ALOGE("Can't find MtpPropertyList.mObjectHandles");
-        return -1;
-    }
-    field_mPropertyCodes = env->GetFieldID(clazz, "mPropertyCodes", "[I");
-    if (field_mPropertyCodes == NULL) {
-        ALOGE("Can't find MtpPropertyList.mPropertyCodes");
-        return -1;
-    }
-    field_mDataTypes = env->GetFieldID(clazz, "mDataTypes", "[I");
-    if (field_mDataTypes == NULL) {
-        ALOGE("Can't find MtpPropertyList.mDataTypes");
-        return -1;
-    }
-    field_mLongValues = env->GetFieldID(clazz, "mLongValues", "[J");
-    if (field_mLongValues == NULL) {
-        ALOGE("Can't find MtpPropertyList.mLongValues");
-        return -1;
-    }
-    field_mStringValues = env->GetFieldID(clazz, "mStringValues", "[Ljava/lang/String;");
-    if (field_mStringValues == NULL) {
-        ALOGE("Can't find MtpPropertyList.mStringValues");
-        return -1;
-    }
+    GET_METHOD_ID(getCode, clazz, "()I");
+    GET_METHOD_ID(getCount, clazz, "()I");
+    GET_METHOD_ID(getObjectHandles, clazz, "()[I");
+    GET_METHOD_ID(getPropertyCodes, clazz, "()[I");
+    GET_METHOD_ID(getDataTypes, clazz, "()[I");
+    GET_METHOD_ID(getLongValues, clazz, "()[J");
+    GET_METHOD_ID(getStringValues, clazz, "()[Ljava/lang/String;");
 
     if (AndroidRuntime::registerNativeMethods(env,
                 "android/mtp/MtpDatabase", gMtpDatabaseMethods, NELEM(gMtpDatabaseMethods)))
diff --git a/media/jni/android_mtp_MtpServer.cpp b/media/jni/android_mtp_MtpServer.cpp
index 6ce104d..c76cebe 100644
--- a/media/jni/android_mtp_MtpServer.cpp
+++ b/media/jni/android_mtp_MtpServer.cpp
@@ -41,7 +41,6 @@
 static jfieldID field_MtpStorage_storageId;
 static jfieldID field_MtpStorage_path;
 static jfieldID field_MtpStorage_description;
-static jfieldID field_MtpStorage_reserveSpace;
 static jfieldID field_MtpStorage_removable;
 static jfieldID field_MtpStorage_maxFileSize;
 
@@ -50,7 +49,7 @@
 // ----------------------------------------------------------------------------
 
 // in android_mtp_MtpDatabase.cpp
-extern MtpDatabase* getMtpDatabase(JNIEnv *env, jobject database);
+extern IMtpDatabase* getMtpDatabase(JNIEnv *env, jobject database);
 
 static inline MtpServer* getMtpServer(JNIEnv *env, jobject thiz) {
     return (MtpServer*)env->GetLongField(thiz, field_MtpServer_nativeContext);
@@ -162,7 +161,6 @@
         jint storageID = env->GetIntField(jstorage, field_MtpStorage_storageId);
         jstring path = (jstring)env->GetObjectField(jstorage, field_MtpStorage_path);
         jstring description = (jstring)env->GetObjectField(jstorage, field_MtpStorage_description);
-        jlong reserveSpace = env->GetLongField(jstorage, field_MtpStorage_reserveSpace);
         jboolean removable = env->GetBooleanField(jstorage, field_MtpStorage_removable);
         jlong maxFileSize = env->GetLongField(jstorage, field_MtpStorage_maxFileSize);
 
@@ -171,7 +169,7 @@
             const char *descriptionStr = env->GetStringUTFChars(description, NULL);
             if (descriptionStr != NULL) {
                 MtpStorage* storage = new MtpStorage(storageID, pathStr, descriptionStr,
-                        reserveSpace, removable, maxFileSize);
+                        removable, maxFileSize);
                 server->addStorage(storage);
                 env->ReleaseStringUTFChars(path, pathStr);
                 env->ReleaseStringUTFChars(description, descriptionStr);
@@ -241,11 +239,6 @@
         ALOGE("Can't find MtpStorage.mDescription");
         return -1;
     }
-    field_MtpStorage_reserveSpace = env->GetFieldID(clazz, "mReserveSpace", "J");
-    if (field_MtpStorage_reserveSpace == NULL) {
-        ALOGE("Can't find MtpStorage.mReserveSpace");
-        return -1;
-    }
     field_MtpStorage_removable = env->GetFieldID(clazz, "mRemovable", "Z");
     if (field_MtpStorage_removable == NULL) {
         ALOGE("Can't find MtpStorage.mRemovable");
diff --git a/media/tests/MtpTests/Android.mk b/media/tests/MtpTests/Android.mk
new file mode 100644
index 0000000..616e600
--- /dev/null
+++ b/media/tests/MtpTests/Android.mk
@@ -0,0 +1,12 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
+LOCAL_PACKAGE_NAME := MtpTests
+
+include $(BUILD_PACKAGE)
diff --git a/media/tests/MtpTests/AndroidManifest.xml b/media/tests/MtpTests/AndroidManifest.xml
new file mode 100644
index 0000000..21e2b01
--- /dev/null
+++ b/media/tests/MtpTests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.mtp" >
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
+
+    <uses-permission  android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.mtp"
+                     android:label="MtpTests"/>
+</manifest>
diff --git a/media/tests/MtpTests/AndroidTest.xml b/media/tests/MtpTests/AndroidTest.xml
new file mode 100644
index 0000000..a61a3b4
--- /dev/null
+++ b/media/tests/MtpTests/AndroidTest.xml
@@ -0,0 +1,15 @@
+<configuration description="Runs sample instrumentation test.">
+    <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="MtpTests.apk"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="MtpTests"/>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.mtp"/>
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java b/media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java
new file mode 100644
index 0000000..0d7f3fe
--- /dev/null
+++ b/media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java
@@ -0,0 +1,1657 @@
+/*
+ * 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.os.FileUtils;
+import android.os.UserHandle;
+import android.os.storage.StorageVolume;
+import android.support.test.filters.SmallTest;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.UUID;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Tests for MtpStorageManager functionality.
+ */
+@RunWith(JUnit4.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class MtpStorageManagerTest {
+    private static final String TAG = MtpStorageManagerTest.class.getSimpleName();
+
+    private static final String TEMP_DIR = InstrumentationRegistry.getContext().getFilesDir()
+            + "/" + TAG + "/";
+    private static final File TEMP_DIR_FILE = new File(TEMP_DIR);
+
+    private MtpStorageManager manager;
+
+    private ArrayList<Integer> objectsAdded;
+    private ArrayList<Integer> objectsRemoved;
+
+    private File mainStorageDir;
+    private File secondaryStorageDir;
+
+    private MtpStorage mainMtpStorage;
+    private MtpStorage secondaryMtpStorage;
+
+    static {
+        MtpStorageManager.sDebug = true;
+    }
+
+    private static void logMethodName() {
+        Log.d(TAG, Thread.currentThread().getStackTrace()[3].getMethodName());
+    }
+
+    private static File createNewFile(File parent) {
+        return createNewFile(parent, UUID.randomUUID().toString());
+    }
+
+    private static File createNewFile(File parent, String name) {
+        try {
+            File ret = new File(parent, name);
+            if (!ret.createNewFile())
+                throw new AssertionError("Failed to create file");
+            return ret;
+        } catch (IOException e) {
+            throw new AssertionError(e.getMessage());
+        }
+    }
+
+    private static File createNewDir(File parent, String name) {
+        File ret = new File(parent, name);
+        if (!ret.mkdir())
+            throw new AssertionError("Failed to create file");
+        return ret;
+    }
+
+    private static File createNewDir(File parent) {
+        return createNewDir(parent, UUID.randomUUID().toString());
+    }
+
+    @Before
+    public void before() {
+        Assert.assertTrue(TEMP_DIR_FILE.mkdir());
+        mainStorageDir = createNewDir(TEMP_DIR_FILE);
+        secondaryStorageDir = createNewDir(TEMP_DIR_FILE);
+
+        StorageVolume mainStorage = new StorageVolume("1", mainStorageDir, "", true, false, true,
+                false, -1, UserHandle.CURRENT, "", "");
+        StorageVolume secondaryStorage = new StorageVolume("2", secondaryStorageDir, "", false,
+                false, true, false, -1, UserHandle.CURRENT, "", "");
+
+        objectsAdded = new ArrayList<>();
+        objectsRemoved = new ArrayList<>();
+
+        manager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
+            @Override
+            public void sendObjectAdded(int id) {
+                objectsAdded.add(id);
+            }
+
+            @Override
+            public void sendObjectRemoved(int id) {
+                objectsRemoved.add(id);
+            }
+        }, null);
+
+        mainMtpStorage = manager.addMtpStorage(mainStorage);
+        secondaryMtpStorage = manager.addMtpStorage(secondaryStorage);
+    }
+
+    @After
+    public void after() {
+        manager.close();
+        FileUtils.deleteContentsAndDir(TEMP_DIR_FILE);
+    }
+
+    /** MtpObject getter tests. **/
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetNameRoot() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertEquals(obj.getName(), mainStorageDir.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetNameNonRoot() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getName(), newFile.getName());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetIdRoot() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertEquals(obj.getId(), mainMtpStorage.getStorageId());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetIdNonRoot() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getId(), 1);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectIsDirTrue() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertTrue(obj.isDir());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectIsDirFalse() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir, "TEST123.mp3");
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertFalse(stream.findFirst().get().isDir());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetFormatDir() {
+        logMethodName();
+        File newFile = createNewDir(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getFormat(), MtpConstants.FORMAT_ASSOCIATION);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetFormatNonDir() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir, "TEST123.mp3");
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getFormat(), MtpConstants.FORMAT_MP3);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetStorageId() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getStorageId(), mainMtpStorage.getStorageId());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetLastModified() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getModifiedTime(),
+                newFile.lastModified() / 1000);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetParent() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getParent(),
+                manager.getStorageRoot(mainMtpStorage.getStorageId()));
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetRoot() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getRoot(),
+                manager.getStorageRoot(mainMtpStorage.getStorageId()));
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetPath() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getPath().toString(), newFile.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetSize() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        try {
+            new FileOutputStream(newFile).write(new byte[] {0, 0, 0, 0, 0, 0, 0, 0});
+        } catch (IOException e) {
+            Assert.fail();
+        }
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getSize(), 8);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetSizeDir() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getSize(), 0);
+    }
+
+    /** MtpStorageManager cache access tests. **/
+
+    @Test
+    @SmallTest
+    public void testAddMtpStorage() {
+        logMethodName();
+        Assert.assertEquals(mainMtpStorage.getPath(), mainStorageDir.getPath());
+        Assert.assertNotNull(manager.getStorageRoot(mainMtpStorage.getStorageId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveMtpStorage() {
+        logMethodName();
+        File newFile = createNewFile(secondaryStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                secondaryMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 1);
+
+        manager.removeMtpStorage(secondaryMtpStorage);
+        Assert.assertNull(manager.getStorageRoot(secondaryMtpStorage.getStorageId()));
+        Assert.assertNull(manager.getObject(1));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetByPath() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+
+        MtpStorageManager.MtpObject obj = manager.getByPath(newFile.getPath());
+        Assert.assertNotNull(obj);
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetByPathError() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+
+        MtpStorageManager.MtpObject obj = manager.getByPath(newFile.getPath() + "q");
+        Assert.assertNull(obj);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObject() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+        MtpStorageManager.MtpObject obj = manager.getByPath(newFile.getPath());
+        Assert.assertNotNull(obj);
+
+        Assert.assertEquals(manager.getObject(obj.getId()), obj);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectError() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+
+        Assert.assertNull(manager.getObject(42));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetStorageRoot() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertEquals(obj.getPath().toString(), mainStorageDir.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsParent() {
+        logMethodName();
+        File newDir = createNewDir(createNewDir(mainStorageDir));
+        File newFile = createNewFile(newDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+        MtpStorageManager.MtpObject parent = manager.getByPath(newDir.getPath());
+        Assert.assertNotNull(parent);
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(parent.getId(), 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsFormat() {
+        logMethodName();
+        File newDir = createNewDir(createNewDir(mainStorageDir));
+        File newFile = createNewFile(newDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+        MtpStorageManager.MtpObject parent = manager.getByPath(newDir.getPath());
+        Assert.assertNotNull(parent);
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(parent.getId(),
+                MtpConstants.FORMAT_MP3, mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getPath().toString(), newMP3File.toString());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsRoot() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File newFile = createNewFile(mainStorageDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsAll() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File newFile = createNewFile(mainStorageDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 3);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsAllStorages() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(mainStorageDir);
+        createNewFile(newDir, "lalala.mp3");
+        File newDir2 = createNewDir(secondaryStorageDir);
+        createNewFile(secondaryStorageDir);
+        createNewFile(newDir2, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0, 0, 0xFFFFFFFF);
+        Assert.assertEquals(stream.count(), 6);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsAllStoragesRoot() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(mainStorageDir);
+        createNewFile(newDir, "lalala.mp3");
+        File newDir2 = createNewDir(secondaryStorageDir);
+        createNewFile(secondaryStorageDir);
+        createNewFile(newDir2, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0, 0xFFFFFFFF);
+        Assert.assertEquals(stream.count(), 4);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    /** MtpStorageManager event handling tests. **/
+
+    @Test
+    @SmallTest
+    public void testObjectAdded() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 0);
+
+        File newFile = createNewFile(mainStorageDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newFile.getPath());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectAddedDir() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 0);
+
+        File newDir = createNewDir(mainStorageDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newDir.getPath());
+        Assert.assertTrue(manager.getObject(objectsAdded.get(0)).isDir());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectAddedRecursiveDir() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 0);
+
+        File newDir = createNewDir(createNewDir(createNewDir(mainStorageDir)));
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 3);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(2)).getPath().toString(),
+                newDir.getPath());
+        Assert.assertTrue(manager.getObject(objectsAdded.get(2)).isDir());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 1);
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertNull(manager.getObject(objectsRemoved.get(0)));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectMoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 1);
+        File toFile = new File(mainStorageDir, "to" + newFile.getName());
+
+        Assert.assertTrue(newFile.renameTo(toFile));
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                toFile.getPath());
+        Assert.assertNull(manager.getObject(objectsRemoved.get(0)));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    /** MtpStorageManager operation tests. Ensure that events are not sent for the main operation,
+        and also test all possible cases of other processes accessing the file at the same time, as
+        well as cases of both failure and success. **/
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccess() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDir() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newDir", MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertEquals(id, 1);
+
+        File newFile = createNewDir(mainStorageDir, "newDir");
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(obj.getFormat(), MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Check that new dir receives events
+        File newerFile = createNewFile(newFile);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newerFile.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDelayed() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        manager.flushEvents();
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDirDelayed() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newDir", MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertEquals(id, 1);
+
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        File newFile = createNewDir(mainStorageDir, "newDir");
+        manager.flushEvents();
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(obj.getFormat(), MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Check that new dir receives events
+        File newerFile = createNewFile(newFile);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newerFile.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDeleted() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertEquals(objectsRemoved.get(0).intValue(), obj.getId());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectFailed() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, false));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectFailedDeleted() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endSendObject(obj, false));
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectFailedAdded() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+
+        File newDir = createNewDir(mainStorageDir, "newFile");
+        manager.flushEvents();
+        Assert.assertTrue(manager.endSendObject(obj, false));
+        Assert.assertNotEquals(objectsAdded.get(0).intValue(), id);
+        Assert.assertNull(manager.getObject(id));
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newDir.getPath());
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events in new dir
+        createNewFile(newDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectDelayed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectDir() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(createNewDir(newDir));
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        createNewFile(newDir);
+        Assert.assertTrue(FileUtils.deleteContentsAndDir(newDir));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertEquals(manager.getObjects(0, 0, mainMtpStorage.getStorageId()).count(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectDirDelayed() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(createNewDir(newDir));
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertTrue(FileUtils.deleteContentsAndDir(newDir));
+        manager.flushEvents();
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObjects(0, 0, mainMtpStorage.getStorageId()).count(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectSuccessAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        int id = obj.getId();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(newFile.delete());
+        createNewFile(mainStorageDir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertNull(manager.getObject(id));
+        Assert.assertNotEquals(objectsAdded.get(0).intValue(), id);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(manager.endRemoveObject(obj, false));
+        Assert.assertEquals(manager.getObject(obj.getId()), obj);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectFailedDir() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        createNewFile(newDir);
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, false));
+        Assert.assertEquals(manager.getObject(obj.getId()), obj);
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectFailedRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, false));
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertEquals(objectsRemoved.get(0).intValue(), obj.getId());
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        createNewFile(newDir, newFile.getName());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, true));
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectSuccessRecursive() {
+        logMethodName();
+        File newDirFrom = createNewDir(mainStorageDir);
+        File newDirFrom1 = createNewDir(newDirFrom);
+        File newDirFrom2 = createNewFile(newDirFrom1);
+        File delayedFile = createNewFile(newDirFrom);
+        File deletedFile = createNewFile(newDirFrom);
+        File newDirTo = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject toObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDirTo.getName())).findFirst().get();
+        MtpStorageManager.MtpObject fromObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDirFrom.getName())).findFirst().get();
+
+        manager.getObjects(fromObj.getId(), 0, mainMtpStorage.getStorageId());
+        int id = manager.beginCopyObject(fromObj, toObj);
+        Assert.assertNotEquals(id, -1);
+        File copiedDir = createNewDir(newDirTo, newDirFrom.getName());
+        File copiedDir1 = createNewDir(copiedDir, newDirFrom1.getName());
+        createNewFile(copiedDir1, newDirFrom2.getName());
+        createNewFile(copiedDir, "extraFile");
+        File toDelete = createNewFile(copiedDir, deletedFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(toDelete.delete());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, true));
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        createNewFile(copiedDir, delayedFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events in the visited dir, but not the unvisited dir.
+        createNewFile(copiedDir);
+        createNewFile(copiedDir1);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 2);
+        Assert.assertEquals(objectsAdded.size(), 2);
+
+        // Number of files/dirs created, minus the one that was deleted.
+        Assert.assertEquals(manager.getObjects(0, 0, mainMtpStorage.getStorageId()).count(), 13);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, false));
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectFailedAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        File addedDir = createNewDir(newDir, newFile.getName());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, false));
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertNotEquals(objectsAdded.get(0).intValue(), id);
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events in new dir
+        createNewFile(addedDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectFailedDeleted() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        Assert.assertTrue(createNewFile(newDir, newFile.getName()).delete());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, false));
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newFile.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDirSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Don't expect events
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDirVisitedSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDelayed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), true));
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newFile.renameTo(renamed));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDirVisitedDelayed() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(manager.endRenameObject(obj, newDir.getName(), true));
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newDir.renameTo(renamed));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectFailedOldRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectFailedNewAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        createNewFile(mainStorageDir, "renamed");
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        File moved = new File(dir, newFile.getName());
+        Assert.assertTrue(newFile.renameTo(moved));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(fileObj.getPath().toString(), moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDirSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDir.getName())).findFirst().get();
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(movedDir.getName())).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(movedObj, dirObj));
+
+        File renamed = new File(newDir, movedDir.getName());
+        Assert.assertTrue(movedDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(movedObj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Don't expect events
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDirVisitedSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDir.getName())).findFirst().get();
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(movedDir.getName())).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj, dirObj));
+
+        File renamed = new File(newDir, movedDir.getName());
+        Assert.assertTrue(movedDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(movedObj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDelayed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), true));
+
+        File moved = new File(dir, newFile.getName());
+        Assert.assertTrue(newFile.renameTo(moved));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(fileObj.getPath().toString(), moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDirVisitedDelayed() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDir.getName())).findFirst().get();
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(movedDir.getName())).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj, dirObj));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, movedDir.getName(), true));
+
+        File renamed = new File(newDir, movedDir.getName());
+        Assert.assertTrue(movedDir.renameTo(renamed));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(movedObj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectFailedOldRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectFailedNewAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        createNewFile(dir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(newFile.delete());
+        File moved = createNewFile(secondaryStorageDir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(fileObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDirSuccess() {
+        logMethodName();
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(movedDir.delete());
+        File moved = createNewDir(secondaryStorageDir, movedDir.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Don't expect events
+        createNewFile(moved);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDirVisitedSuccess() {
+        logMethodName();
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(movedDir.delete());
+        File moved = createNewDir(secondaryStorageDir, movedDir.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(moved);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDelayed() {
+        logMethodName();
+        File movedFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedFile.getName(), true));
+
+        Assert.assertTrue(movedFile.delete());
+        File moved = createNewFile(secondaryStorageDir, movedFile.getName());
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDirVisitedDelayed() {
+        logMethodName();
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedDir.getName(), true));
+
+        Assert.assertTrue(movedDir.delete());
+        File moved = createNewDir(secondaryStorageDir, movedDir.getName());
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(moved);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageFailedOldRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageFailedNewAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        createNewFile(secondaryStorageDir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 7f0b508..ac800e7 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2691,15 +2691,14 @@
             final boolean primary = true;
             final boolean removable = primaryPhysical;
             final boolean emulated = !primaryPhysical;
-            final long mtpReserveSize = 0L;
             final boolean allowMassStorage = false;
             final long maxFileSize = 0L;
             final UserHandle owner = new UserHandle(userId);
             final String uuid = null;
             final String state = Environment.MEDIA_REMOVED;
 
-            res.add(0, new StorageVolume(id, StorageVolume.STORAGE_ID_INVALID, path,
-                    description, primary, removable, emulated, mtpReserveSize,
+            res.add(0, new StorageVolume(id, path,
+                    description, primary, removable, emulated,
                     allowMassStorage, maxFileSize, owner, uuid, state));
         }