diff options
-rw-r--r-- | core/java/android/os/storage/StorageVolume.java | 43 | ||||
-rw-r--r-- | core/java/android/os/storage/VolumeInfo.java | 20 | ||||
-rw-r--r-- | core/java/android/provider/MediaStore.java | 9 | ||||
-rwxr-xr-x | media/java/android/mtp/MtpDatabase.java | 1339 | ||||
-rw-r--r-- | media/java/android/mtp/MtpPropertyGroup.java | 404 | ||||
-rw-r--r-- | media/java/android/mtp/MtpPropertyList.java | 95 | ||||
-rw-r--r-- | media/java/android/mtp/MtpStorage.java | 18 | ||||
-rw-r--r-- | media/java/android/mtp/MtpStorageManager.java | 1210 | ||||
-rw-r--r-- | media/jni/android_mtp_MtpDatabase.cpp | 654 | ||||
-rw-r--r-- | media/jni/android_mtp_MtpServer.cpp | 11 | ||||
-rw-r--r-- | media/tests/MtpTests/Android.mk | 12 | ||||
-rw-r--r-- | media/tests/MtpTests/AndroidManifest.xml | 31 | ||||
-rw-r--r-- | media/tests/MtpTests/AndroidTest.xml | 15 | ||||
-rw-r--r-- | media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java | 1657 | ||||
-rw-r--r-- | services/core/java/com/android/server/StorageManagerService.java | 5 |
15 files changed, 3967 insertions, 1556 deletions
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index 1fc0b820cf06..070b8c1b0008 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -19,7 +19,6 @@ package android.os.storage; 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 @@ import java.io.File; 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 final class StorageVolume implements Parcelable { 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 @@ public final class StorageVolume implements Parcelable { 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 @@ public final class StorageVolume implements Parcelable { } /** - * 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 @@ public final class StorageVolume implements Parcelable { 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 @@ public final class StorageVolume implements Parcelable { @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 76f79f13d9a7..d3877cac17b0 100644 --- a/core/java/android/os/storage/VolumeInfo.java +++ b/core/java/android/os/storage/VolumeInfo.java @@ -343,9 +343,7 @@ public class VolumeInfo implements Parcelable { 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 @@ public class VolumeInfo implements Parcelable { 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 @@ public class VolumeInfo implements Parcelable { 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 @@ public class VolumeInfo implements Parcelable { 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 32d68cd9f869..d9808a3d1412 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -63,15 +63,6 @@ public final class MediaStore { 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 ba29d2daaa0e..a647dcc2d4b9 100755 --- a/media/java/android/mtp/MtpDatabase.java +++ b/media/java/android/mtp/MtpDatabase.java @@ -30,6 +30,7 @@ import android.net.Uri; 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 android.view.WindowManager; 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,86 +74,158 @@ public class MtpDatabase implements AutoCloseable { 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 + // 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 static final int[] PLAYBACK_FORMATS = { + // allow transferring arbitrary files + MtpConstants.FORMAT_UNDEFINED, + + MtpConstants.FORMAT_ASSOCIATION, + MtpConstants.FORMAT_TEXT, + MtpConstants.FORMAT_HTML, + MtpConstants.FORMAT_WAV, + MtpConstants.FORMAT_MP3, + MtpConstants.FORMAT_MPEG, + MtpConstants.FORMAT_EXIF_JPEG, + MtpConstants.FORMAT_TIFF_EP, + MtpConstants.FORMAT_BMP, + MtpConstants.FORMAT_GIF, + MtpConstants.FORMAT_JFIF, + MtpConstants.FORMAT_PNG, + MtpConstants.FORMAT_TIFF, + MtpConstants.FORMAT_WMA, + MtpConstants.FORMAT_OGG, + MtpConstants.FORMAT_AAC, + MtpConstants.FORMAT_MP4_CONTAINER, + MtpConstants.FORMAT_MP2, + MtpConstants.FORMAT_3GP_CONTAINER, + MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, + MtpConstants.FORMAT_WPL_PLAYLIST, + MtpConstants.FORMAT_M3U_PLAYLIST, + MtpConstants.FORMAT_PLS_PLAYLIST, + MtpConstants.FORMAT_XML_DOCUMENT, + MtpConstants.FORMAT_FLAC, + MtpConstants.FORMAT_DNG, + MtpConstants.FORMAT_HEIF, }; - private static final String[] PATH_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.DATA, // 1 + + 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_PERSISTENT_UID, + MtpConstants.PROPERTY_PARENT_OBJECT, + MtpConstants.PROPERTY_NAME, + MtpConstants.PROPERTY_DISPLAY_NAME, + MtpConstants.PROPERTY_DATE_ADDED, }; - private static final String[] FORMAT_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.FORMAT, // 1 + + private static final int[] AUDIO_PROPERTIES = { + MtpConstants.PROPERTY_ARTIST, + MtpConstants.PROPERTY_ALBUM_NAME, + MtpConstants.PROPERTY_ALBUM_ARTIST, + MtpConstants.PROPERTY_TRACK, + MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, + MtpConstants.PROPERTY_DURATION, + MtpConstants.PROPERTY_GENRE, + MtpConstants.PROPERTY_COMPOSER, + MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, + MtpConstants.PROPERTY_BITRATE_TYPE, + MtpConstants.PROPERTY_AUDIO_BITRATE, + MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, + MtpConstants.PROPERTY_SAMPLE_RATE, }; - private static final String[] PATH_FORMAT_PROJECTION = new String[] { - Files.FileColumns._ID, // 0 - Files.FileColumns.DATA, // 1 - Files.FileColumns.FORMAT, // 2 + + private static final int[] VIDEO_PROPERTIES = { + MtpConstants.PROPERTY_ARTIST, + MtpConstants.PROPERTY_ALBUM_NAME, + MtpConstants.PROPERTY_DURATION, + MtpConstants.PROPERTY_DESCRIPTION, }; - 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 int[] IMAGE_PROPERTIES = { + MtpConstants.PROPERTY_DESCRIPTION, }; - 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 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 MtpServer mServer; + private int[] getSupportedObjectProperties(int format) { + switch (format) { + case MtpConstants.FORMAT_MP3: + case MtpConstants.FORMAT_WAV: + case MtpConstants.FORMAT_WMA: + case MtpConstants.FORMAT_OGG: + case MtpConstants.FORMAT_AAC: + 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 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 IntStream.concat(Arrays.stream(FILE_PROPERTIES), + Arrays.stream(IMAGE_PROPERTIES)).toArray(); + default: + return FILE_PROPERTIES; + } + } - // read from native code - private int mBatteryLevel; - private int mBatteryScale; + private int[] getSupportedDeviceProperties() { + return DEVICE_PROPERTIES; + } - private int mDeviceType; + private int[] getSupportedPlaybackFormats() { + return PLAYBACK_FORMATS; + } - static { - System.loadLibrary("media_jni"); + private int[] getSupportedCaptureFormats() { + // no capture formats yet + return null; } private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { - @Override + @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { @@ -160,61 +243,42 @@ public class MtpDatabase implements AutoCloseable { } }; - public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath, + public MtpDatabase(Context context, Context userContext, String volumeName, 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 "); - } + mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() { + @Override + public void sendObjectAdded(int id) { + if (MtpDatabase.this.mServer != null) + MtpDatabase.this.mServer.sendObjectAdded(id); } - 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 + "/%"; + + @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, @@ -224,6 +288,7 @@ public class MtpDatabase implements AutoCloseable { @Override public void close() { + mManager.close(); mCloseGuard.close(); if (mClosed.compareAndSet(false, true)) { mMediaScanner.close(); @@ -238,24 +303,32 @@ public class MtpDatabase implements AutoCloseable { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } - close(); } finally { super.finalize(); } } - public void addStorage(MtpStorage storage) { - mStorageMap.put(storage.getPath(), storage); + public void addStorage(StorageVolume storage) { + MtpStorage mtpStorage = mManager.addMtpStorage(storage); + mStorageMap.put(storage.getPath(), mtpStorage); + mServer.addStorage(mtpStorage); } - public void removeStorage(MtpStorage storage) { + 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); + mDeviceProperties = context.getSharedPreferences(devicePropertiesName, + Context.MODE_PRIVATE); File databaseFile = context.getDatabasePath(devicePropertiesName); if (databaseFile.exists()) { @@ -266,7 +339,7 @@ public class MtpDatabase implements AutoCloseable { try { db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null); if (db != null) { - c = db.query("properties", new String[] { "_id", "code", "value" }, + c = db.query("properties", new String[]{"_id", "code", "value"}, null, null, null, null, null); if (c != null) { SharedPreferences.Editor e = mDeviceProperties.edit(); @@ -288,608 +361,371 @@ public class MtpDatabase implements AutoCloseable { } } - // 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; - } + 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; } - 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; + Path objPath = Paths.get(path); + return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format); } - 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; + 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; } - // 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; + // 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 { - 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; + 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); - } 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); - } + 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 { - 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); + 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 { - deleteFile(handle); + mMediaScanner.scanMtpFile(path, handle, format); } } - private void doScanDirectory(String path) { - String[] scanPath; - scanPath = new String[] { path }; - mMediaScanner.scanDirectories(scanPath); + 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 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)}; - } - } - } + 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(); + } - // 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; + private MtpPropertyList getObjectPropertyList(int handle, int format, int property, + int groupCode, int depth) { + // FIXME - implement group support + if (property == 0) { + if (groupCode == 0) { + return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED); } + return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); } - - 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; + 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); } - 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; + } 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); } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectList", e); - } finally { - if (c != null) { - c.close(); + if (obj.getFormat() == format || format == 0) { + objectStream = Stream.of(obj); } } - 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(); + if (handle == 0 || depth == 1) { + if (handle == 0) { + handle = 0xFFFFFFFF; } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getNumObjects", e); - } finally { - if (c != null) { - c.close(); + // 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); } - } - return -1; - } - - private int[] getSupportedPlaybackFormats() { - return new int[] { - // allow transfering arbitrary files - MtpConstants.FORMAT_UNDEFINED, - - MtpConstants.FORMAT_ASSOCIATION, - MtpConstants.FORMAT_TEXT, - MtpConstants.FORMAT_HTML, - MtpConstants.FORMAT_WAV, - MtpConstants.FORMAT_MP3, - MtpConstants.FORMAT_MPEG, - MtpConstants.FORMAT_EXIF_JPEG, - MtpConstants.FORMAT_TIFF_EP, - MtpConstants.FORMAT_BMP, - MtpConstants.FORMAT_GIF, - MtpConstants.FORMAT_JFIF, - MtpConstants.FORMAT_PNG, - MtpConstants.FORMAT_TIFF, - MtpConstants.FORMAT_WMA, - MtpConstants.FORMAT_OGG, - MtpConstants.FORMAT_AAC, - MtpConstants.FORMAT_MP4_CONTAINER, - MtpConstants.FORMAT_MP2, - MtpConstants.FORMAT_3GP_CONTAINER, - MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, - MtpConstants.FORMAT_WPL_PLAYLIST, - MtpConstants.FORMAT_M3U_PLAYLIST, - MtpConstants.FORMAT_PLS_PLAYLIST, - MtpConstants.FORMAT_XML_DOCUMENT, - 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 - 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, - }; - - 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 - MtpConstants.PROPERTY_ARTIST, - MtpConstants.PROPERTY_ALBUM_NAME, - MtpConstants.PROPERTY_ALBUM_ARTIST, - MtpConstants.PROPERTY_TRACK, - MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, - MtpConstants.PROPERTY_DURATION, - MtpConstants.PROPERTY_GENRE, - MtpConstants.PROPERTY_COMPOSER, - MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, - MtpConstants.PROPERTY_BITRATE_TYPE, - MtpConstants.PROPERTY_AUDIO_BITRATE, - MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, - 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 - 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 - MtpConstants.PROPERTY_DESCRIPTION, - }; - - private int[] getSupportedObjectProperties(int format) { - switch (format) { - case MtpConstants.FORMAT_MP3: - case MtpConstants.FORMAT_WAV: - case MtpConstants.FORMAT_WMA: - case MtpConstants.FORMAT_OGG: - case MtpConstants.FORMAT_AAC: - return AUDIO_PROPERTIES; - case MtpConstants.FORMAT_MPEG: - case MtpConstants.FORMAT_3GP_CONTAINER: - case MtpConstants.FORMAT_WMV: - return VIDEO_PROPERTIES; - 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; - 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, - }; - } - - private MtpPropertyList getObjectPropertyList(int handle, int format, int property, - int groupCode, int depth) { - // FIXME - implement group support - if (groupCode != 0) { - return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); + 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); - } - propertyGroup = mPropertyGroupsByFormat.get(format); - if (propertyGroup == null) { - int[] propertyList = getSupportedObjectProperties(format); - propertyGroup = new MtpPropertyGroup(this, mMediaProvider, - mVolumeName, propertyList); - mPropertyGroupsByFormat.put(format, propertyGroup); + 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); + } } - } 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; + + boolean allowed = mManager.beginMoveObject(obj, parent); + return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR; + } - // do not allow renaming any of the special subdirectories - if (isStorageSubDirectory(newPath)) { - return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; + 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; + } + 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); } - return MtpConstants.RESPONSE_OK; } 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 @@ public class MtpDatabase implements AutoCloseable { 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 @@ public class MtpDatabase implements AutoCloseable { } 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); + + 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) { + MtpStorageManager.MtpObject obj = mManager.getObject(handle); + if (obj == null) { + return -1; + } + return obj.getFormat(); + } + + 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; - } finally { - if (c != null) { - c.close(); - } } + return MtpConstants.RESPONSE_OK; } - private int getObjectFormat(int handle) { + 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, 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()) { - return c.getInt(1); - } else { - return -1; + ret = c.getInt(0); } } catch (RemoteException e) { - Log.e(TAG, "RemoteException in getObjectFilePath", e); - return -1; + Log.e(TAG, "Error finding " + path + " in MediaProvider"); } finally { - if (c != null) { + if (c != null) c.close(); - } } + return ret; } - private int deleteFile(int handle) { - mDatabaseModified = true; - String path = null; - int format = 0; - - Cursor c = null; + private void deleteFromMedia(Path path, boolean isDir) { try { - c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, - ID_WHERE, new String[] { Integer.toString(handle) }, 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; - } - - 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) { + // 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; - } - } catch (RemoteException e) { - Log.e(TAG, "RemoteException in deleteFile", e); - return MtpConstants.RESPONSE_GENERAL_ERROR; - } finally { - if (c != null) { - c.close(); + Log.i(TAG, "Mediaprovider didn't delete " + path); } + } 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 @@ public class MtpDatabase implements AutoCloseable { } 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 @@ public class MtpDatabase implements AutoCloseable { 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 dea300838385..77d0f34f1ad6 100644 --- a/media/java/android/mtp/MtpPropertyGroup.java +++ b/media/java/android/mtp/MtpPropertyGroup.java @@ -23,22 +23,21 @@ import android.os.RemoteException; 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 @@ class MtpPropertyGroup { } } - 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 PATH_WHERE = Files.FileColumns.DATA + "=?"; - 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; // 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 @@ class MtpPropertyGroup { 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 @@ class MtpPropertyGroup { 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 @@ class MtpPropertyGroup { 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 @@ class MtpPropertyGroup { } } - 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 @@ class MtpPropertyGroup { } } - 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 @@ class MtpPropertyGroup { } } - private Long queryLong(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 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; - } - } - } 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; - } - } - } - + /** + * 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 { - // 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 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"); } } - - 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); - 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); + 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_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 f9bc603e3de0..ede90dac517c 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 @@ package android.mtp; 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; - - // 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 + private List<String> mStringValues; + + // 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 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 void setResult(int result) { - mResult = result; + 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 6ca442c7e66f..c72b827d8a2d 100644 --- a/media/java/android/mtp/MtpStorage.java +++ b/media/java/android/mtp/MtpStorage.java @@ -31,15 +31,13 @@ public class MtpStorage { 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 @@ public class MtpStorage { } /** - * 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 000000000000..bdc87413288a --- /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 4e8c72bae9b9..23ef84f6ad90 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 @@ using namespace android; 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_setDeviceProperty; 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; - - -MtpDatabase* getMtpDatabase(JNIEnv *env, jobject database) { - return (MtpDatabase *)env->GetLongField(database, field_context); + +// 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; + + +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 @@ private: 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, - MtpObjectHandle handle, - MtpObjectFormat format, - bool succeeded); + virtual void endSendObject(MtpObjectHandle handle, bool succeeded); - virtual void doScanDirectory(const char* path); + virtual void rescanFile(const char* path, + MtpObjectHandle handle, + MtpObjectFormat format); virtual MtpObjectHandleList* getObjectList(MtpStorageID storageID, MtpObjectFormat format, @@ -167,7 +163,8 @@ public: 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 @@ public: 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 endMoveObject(MtpObjectHandle oldParent, MtpObjectHandle newParent, + MtpStorageID oldStorage, MtpStorageID newStorage, + MtpObjectHandle handle, bool succeeded); - virtual void sessionStarted(); + virtual MtpResponseCode beginCopyObject(MtpObjectHandle handle, MtpObjectHandle newParent, + MtpStorageID newStorage); + virtual void endCopyObject(MtpObjectHandle handle, bool succeeded); - virtual void sessionEnded(); }; // ---------------------------------------------------------------------------- @@ -202,7 +204,7 @@ static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodNa // ---------------------------------------------------------------------------- -MyMtpDatabase::MyMtpDatabase(JNIEnv *env, jobject client) +MtpDatabase::MtpDatabase(JNIEnv *env, jobject client) : mDatabase(env->NewGlobalRef(client)), mIntBuffer(NULL), mLongBuffer(NULL), @@ -228,27 +230,24 @@ MyMtpDatabase::MyMtpDatabase(JNIEnv *env, jobject client) 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 @@ MtpObjectHandle MyMtpDatabase::beginSendObject(const char* path, 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(); - jstring pathStr = env->NewStringUTF(path); - env->CallVoidMethod(mDatabase, method_endSendObject, pathStr, - (jint)handle, (jint)format, (jboolean)succeeded); + env->CallVoidMethod(mDatabase, method_endSendObject, (jint)handle, (jboolean)succeeded); - if (pathStr) - env->DeleteLocalRef(pathStr); checkAndClearExceptionFromCallback(env, __FUNCTION__); } -void MyMtpDatabase::doScanDirectory(const char* path) { +void MtpDatabase::rescanFile(const char* path, MtpObjectHandle handle, + MtpObjectFormat format) { JNIEnv* env = AndroidRuntime::getJNIEnv(); jstring pathStr = env->NewStringUTF(path); - env->CallVoidMethod(mDatabase, method_doScanDirectory, pathStr); + env->CallVoidMethod(mDatabase, method_rescanFile, pathStr, + (jint)handle, (jint)format); 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 @@ MtpObjectHandleList* MyMtpDatabase::getObjectList(MtpStorageID storageID, return list; } -int MyMtpDatabase::getNumObjects(MtpStorageID storageID, +int MtpDatabase::getNumObjects(MtpStorageID storageID, MtpObjectFormat format, MtpObjectHandle parent) { JNIEnv* env = AndroidRuntime::getJNIEnv(); @@ -309,7 +305,7 @@ int MyMtpDatabase::getNumObjects(MtpStorageID storageID, return result; } -MtpObjectFormatList* MyMtpDatabase::getSupportedPlaybackFormats() { +MtpObjectFormatList* MtpDatabase::getSupportedPlaybackFormats() { JNIEnv* env = AndroidRuntime::getJNIEnv(); jintArray array = (jintArray)env->CallObjectMethod(mDatabase, method_getSupportedPlaybackFormats); @@ -327,7 +323,7 @@ MtpObjectFormatList* MyMtpDatabase::getSupportedPlaybackFormats() { return list; } -MtpObjectFormatList* MyMtpDatabase::getSupportedCaptureFormats() { +MtpObjectFormatList* MtpDatabase::getSupportedCaptureFormats() { JNIEnv* env = AndroidRuntime::getJNIEnv(); jintArray array = (jintArray)env->CallObjectMethod(mDatabase, method_getSupportedCaptureFormats); @@ -345,7 +341,7 @@ MtpObjectFormatList* MyMtpDatabase::getSupportedCaptureFormats() { 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 @@ MtpObjectPropertyList* MyMtpDatabase::getSupportedObjectProperties(MtpObjectForm return list; } -MtpDevicePropertyList* MyMtpDatabase::getSupportedDeviceProperties() { +MtpDevicePropertyList* MtpDatabase::getSupportedDeviceProperties() { JNIEnv* env = AndroidRuntime::getJNIEnv(); jintArray array = (jintArray)env->CallObjectMethod(mDatabase, method_getSupportedDeviceProperties); @@ -381,7 +377,7 @@ MtpDevicePropertyList* MyMtpDatabase::getSupportedDeviceProperties() { 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 @@ MtpResponseCode MyMtpDatabase::getObjectPropertyValue(MtpObjectHandle handle, 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 @@ MtpResponseCode MyMtpDatabase::getObjectPropertyValue(MtpObjectHandle handle, 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 @@ static bool readLongValue(int type, MtpDataPacket& packet, jlong& longValue) { return true; } -MtpResponseCode MyMtpDatabase::setObjectPropertyValue(MtpObjectHandle handle, +MtpResponseCode MtpDatabase::setObjectPropertyValue(MtpObjectHandle handle, MtpObjectProperty property, MtpDataPacket& packet) { int type; @@ -590,80 +566,73 @@ fail: 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; - } + 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); + 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; + 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; } - - checkAndClearExceptionFromCallback(env, __FUNCTION__); - return MTP_RESPONSE_OK; + 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 @@ fail: 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 @@ MtpResponseCode MyMtpDatabase::getObjectPropertyList(MtpObjectHandle handle, 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 @@ MtpResponseCode MyMtpDatabase::getObjectPropertyList(MtpObjectHandle handle, 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 @@ MtpResponseCode MyMtpDatabase::getObjectPropertyList(MtpObjectHandle handle, 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 @@ static long getLongFromExifEntry(ExifEntry *e) { 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 @@ MtpResponseCode MyMtpDatabase::getObjectInfo(MtpObjectHandle handle, 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 @@ void* MyMtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) return result; } -MtpResponseCode MyMtpDatabase::getObjectFilePath(MtpObjectHandle handle, +MtpResponseCode MtpDatabase::getObjectFilePath(MtpObjectHandle handle, MtpString& outFilePath, int64_t& outFileLength, MtpObjectFormat& outFormat) { @@ -1005,26 +971,60 @@ MtpResponseCode MyMtpDatabase::getObjectFilePath(MtpObjectHandle handle, 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(); + env->CallVoidMethod(mDatabase, method_endDeleteObject, (jint)handle, (jboolean) succeeded); + + checkAndClearExceptionFromCallback(env, __FUNCTION__); +} + +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(); - jstring stringValue = env->NewStringUTF((const char *) newPath); - MtpResponseCode result = env->CallIntMethod(mDatabase, method_moveObject, - (jint)handle, (jint)newParent, (jint) newStorage, stringValue); + MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginCopyObject, + (jint)handle, (jint)newParent, (jint) newStorage); checkAndClearExceptionFromCallback(env, __FUNCTION__); - env->DeleteLocalRef(stringValue); 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 @@ static const PropertyTableEntry kDevicePropertyTable[] = { { 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 @@ bool MyMtpDatabase::getObjectPropertyInfo(MtpObjectProperty property, int& type) 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 @@ bool MyMtpDatabase::getDevicePropertyInfo(MtpDeviceProperty property, int& type) 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 @@ MtpObjectHandleList* MyMtpDatabase::getObjectReferences(MtpObjectHandle handle) 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 @@ MtpResponseCode MyMtpDatabase::setObjectReferences(MtpObjectHandle handle, return result; } -MtpProperty* MyMtpDatabase::getObjectPropertyDesc(MtpObjectProperty property, +MtpProperty* MtpDatabase::getObjectPropertyDesc(MtpObjectProperty property, MtpObjectFormat format) { static const int channelEnum[] = { 1, // mono @@ -1210,67 +1210,65 @@ MtpProperty* MyMtpDatabase::getObjectPropertyDesc(MtpObjectProperty property, 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 @@ android_mtp_MtpDatabase_setup(JNIEnv *env, jobject thiz) 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 @@ static const JNINativeMethod gMtpPropertyGroupMethods[] = { (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 @@ int register_android_mtp_MtpDatabase(JNIEnv *env) 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 6ce104d01a9e..c76cebebc730 100644 --- a/media/jni/android_mtp_MtpServer.cpp +++ b/media/jni/android_mtp_MtpServer.cpp @@ -41,7 +41,6 @@ static jfieldID field_MtpServer_nativeContext; 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 @@ static Mutex sMutex; // ---------------------------------------------------------------------------- // 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 @@ android_mtp_MtpServer_add_storage(JNIEnv *env, jobject thiz, jobject jstorage) 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 @@ android_mtp_MtpServer_add_storage(JNIEnv *env, jobject thiz, jobject jstorage) 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 @@ int register_android_mtp_MtpServer(JNIEnv *env) 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 000000000000..616e600ad6e7 --- /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 000000000000..21e2b0115878 --- /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 000000000000..a61a3b49c8f7 --- /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 000000000000..0d7f3feaaae6 --- /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 7f0b50817d56..ac800e727462 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -2691,15 +2691,14 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon 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)); } |