diff options
Diffstat (limited to 'media/java/android/mtp/MtpDatabase.java')
-rwxr-xr-x | media/java/android/mtp/MtpDatabase.java | 1339 |
1 files changed, 570 insertions, 769 deletions
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; |