/* * Copyright (C) 2010 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.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.media.ApplicationMediaCapabilities; import android.media.ExifInterface; import android.media.MediaFormat; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.BatteryManager; import android.os.Bundle; import android.os.RemoteException; import android.os.SystemProperties; import android.os.storage.StorageVolume; import android.provider.MediaStore; import android.provider.MediaStore.Files; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.WindowManager; import com.android.internal.annotations.VisibleForNative; import com.android.internal.annotations.VisibleForTesting; import dalvik.system.CloseGuard; import com.google.android.collect.Sets; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; 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.List; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.IntStream; /** * 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.class.getSimpleName(); private static final int MAX_THUMB_SIZE = (200 * 1024); private final Context mContext; private final ContentProviderClient mMediaProvider; private final AtomicBoolean mClosed = new AtomicBoolean(); private final CloseGuard mCloseGuard = CloseGuard.get(); private final HashMap mStorageMap = new HashMap<>(); // cached property groups for single properties private final SparseArray mPropertyGroupsByProperty = new SparseArray<>(); // cached property groups for all properties for a given format private final SparseArray mPropertyGroupsByFormat = new SparseArray<>(); // SharedPreferences for writable MTP device properties private SharedPreferences mDeviceProperties; // Cached device properties private int mBatteryLevel; private int mBatteryScale; private int mDeviceType; private String mHostType; private boolean mSkipThumbForHost = false; private volatile boolean mHostIsWindows = false; private MtpServer mServer; private MtpStorageManager mManager; private static final String PATH_WHERE = 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 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 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 int[] VIDEO_PROPERTIES = { MtpConstants.PROPERTY_ARTIST, MtpConstants.PROPERTY_ALBUM_NAME, MtpConstants.PROPERTY_DURATION, MtpConstants.PROPERTY_DESCRIPTION, }; private static final int[] IMAGE_PROPERTIES = { MtpConstants.PROPERTY_DESCRIPTION, }; private static final int[] DEVICE_PROPERTIES = { MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE, MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL, MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE, MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO, }; @VisibleForNative 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; } } public static Uri getObjectPropertiesUri(int format, String volumeName) { switch (format) { case MtpConstants.FORMAT_MP3: case MtpConstants.FORMAT_WAV: case MtpConstants.FORMAT_WMA: case MtpConstants.FORMAT_OGG: case MtpConstants.FORMAT_AAC: return MediaStore.Audio.Media.getContentUri(volumeName); case MtpConstants.FORMAT_MPEG: case MtpConstants.FORMAT_3GP_CONTAINER: case MtpConstants.FORMAT_WMV: return MediaStore.Video.Media.getContentUri(volumeName); 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 MediaStore.Images.Media.getContentUri(volumeName); default: return MediaStore.Files.getContentUri(volumeName); } } @VisibleForNative private int[] getSupportedDeviceProperties() { return DEVICE_PROPERTIES; } @VisibleForNative private int[] getSupportedPlaybackFormats() { return PLAYBACK_FORMATS; } @VisibleForNative private int[] getSupportedCaptureFormats() { // no capture formats yet return null; } private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); if (newLevel != mBatteryLevel) { mBatteryLevel = newLevel; if (mServer != null) { // send device property changed event mServer.sendDevicePropertyChanged( MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL); } } } } }; public MtpDatabase(Context context, String[] subDirectories) { native_setup(); mContext = Objects.requireNonNull(context); mMediaProvider = context.getContentResolver() .acquireContentProviderClient(MediaStore.AUTHORITY); mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() { @Override public void sendObjectAdded(int id) { if (MtpDatabase.this.mServer != null) MtpDatabase.this.mServer.sendObjectAdded(id); } @Override public void sendObjectRemoved(int id) { if (MtpDatabase.this.mServer != null) MtpDatabase.this.mServer.sendObjectRemoved(id); } @Override public void sendObjectInfoChanged(int id) { if (MtpDatabase.this.mServer != null) MtpDatabase.this.mServer.sendObjectInfoChanged(id); } }, subDirectories == null ? null : Sets.newHashSet(subDirectories)); initDeviceProperties(context); mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0); mCloseGuard.open("close"); } public void setServer(MtpServer server) { mServer = server; // always unregister before registering try { mContext.unregisterReceiver(mBatteryReceiver); } catch (IllegalArgumentException e) { // wasn't previously registered, ignore } // register for battery notifications when we are connected if (server != null) { mContext.registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); } } public Context getContext() { return mContext; } @Override public void close() { mManager.close(); mCloseGuard.close(); if (mClosed.compareAndSet(false, true)) { if (mMediaProvider != null) { mMediaProvider.close(); } native_finalize(); } } @Override protected void finalize() throws Throwable { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(); } finally { super.finalize(); } } public void addStorage(StorageVolume storage) { MtpStorage mtpStorage = mManager.addMtpStorage(storage, () -> mHostIsWindows); mStorageMap.put(storage.getPath(), mtpStorage); if (mServer != null) { mServer.addStorage(mtpStorage); } } public void removeStorage(StorageVolume storage) { MtpStorage mtpStorage = mStorageMap.get(storage.getPath()); if (mtpStorage == null) { return; } if (mServer != null) { mServer.removeStorage(mtpStorage); } mManager.removeMtpStorage(mtpStorage); mStorageMap.remove(storage.getPath()); } private void initDeviceProperties(Context context) { final String devicePropertiesName = "device-properties"; mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE); File databaseFile = context.getDatabasePath(devicePropertiesName); if (databaseFile.exists()) { // for backward compatibility - read device properties from sqlite database // and migrate them to shared prefs SQLiteDatabase db = null; Cursor c = null; try { db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null); if (db != null) { c = db.query("properties", new String[]{"_id", "code", "value"}, null, null, null, null, null); if (c != null) { SharedPreferences.Editor e = mDeviceProperties.edit(); while (c.moveToNext()) { String name = c.getString(1); String value = c.getString(2); e.putString(name, value); } e.commit(); } } } catch (Exception e) { Log.e(TAG, "failed to migrate device properties", e); } finally { if (c != null) c.close(); if (db != null) db.close(); } context.deleteDatabase(devicePropertiesName); } mHostType = ""; mSkipThumbForHost = false; mHostIsWindows = false; } @VisibleForNative @VisibleForTesting public int beginSendObject(String path, int format, int parent, int storageId) { MtpStorageManager.MtpObject parentObj = parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent); if (parentObj == null) { return -1; } Path objPath = Paths.get(path); return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format); } @VisibleForNative private void endSendObject(int handle, boolean succeeded) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null || !mManager.endSendObject(obj, succeeded)) { Log.e(TAG, "Failed to successfully end send object"); return; } // Add the new file to MediaProvider if (succeeded) { updateMediaStore(mContext, obj.getPath().toFile()); } } @VisibleForNative private void rescanFile(String path, int handle, int format) { MediaStore.scanFile(mContext.getContentResolver(), new File(path)); } @VisibleForNative private int[] getObjectList(int storageID, int format, int parent) { List objs = mManager.getObjects(parent, format, storageID); if (objs == null) { return null; } int[] ret = new int[objs.size()]; for (int i = 0; i < objs.size(); i++) { ret[i] = objs.get(i).getId(); } return ret; } @VisibleForNative @VisibleForTesting public int getNumObjects(int storageID, int format, int parent) { List objs = mManager.getObjects(parent, format, storageID); if (objs == null) { return -1; } return objs.size(); } @VisibleForNative 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); } 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); } List objs = null; MtpStorageManager.MtpObject thisObj = null; if (handle == 0xFFFFFFFF) { // All objects are requested objs = mManager.getObjects(0, format, 0xFFFFFFFF); if (objs == null) { return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } } else if (handle != 0) { // Add the requested object if format matches MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } if (obj.getFormat() == format || format == 0) { thisObj = obj; } } if (handle == 0 || depth == 1) { if (handle == 0) { handle = 0xFFFFFFFF; } // Get the direct children of root or this object. objs = mManager.getObjects(handle, format, 0xFFFFFFFF); if (objs == null) { return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE); } } if (objs == null) { objs = new ArrayList<>(); } if (thisObj != null) { objs.add(thisObj); } MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK); MtpPropertyGroup propertyGroup; for (MtpStorageManager.MtpObject obj : objs) { if (property == 0xffffffff) { if (format == 0 && handle != 0 && handle != 0xffffffff) { // return properties based on the object's format format = obj.getFormat(); } // Get all properties supported by this object // format should be the same between get & put propertyGroup = mPropertyGroupsByFormat.get(format); if (propertyGroup == null) { final int[] propertyList = getSupportedObjectProperties(format); propertyGroup = new MtpPropertyGroup(propertyList); mPropertyGroupsByFormat.put(format, propertyGroup); } } else { // Get this property value propertyGroup = mPropertyGroupsByProperty.get(property); if (propertyGroup == null) { final int[] propertyList = new int[]{property}; propertyGroup = new MtpPropertyGroup(propertyList); mPropertyGroupsByProperty.put(property, propertyGroup); } } int err = propertyGroup.getPropertyList(mMediaProvider, obj.getVolumeName(), obj, ret); if (err != MtpConstants.RESPONSE_OK) { return new MtpPropertyList(err); } } return ret; } private int renameFile(int handle, String newName) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } Path oldPath = obj.getPath(); // now rename the file. make sure this succeeds before updating database if (!mManager.beginRenameObject(obj, newName)) return MtpConstants.RESPONSE_GENERAL_ERROR; Path newPath = obj.getPath(); boolean success = oldPath.toFile().renameTo(newPath.toFile()); try { Os.access(oldPath.toString(), OsConstants.F_OK); Os.access(newPath.toString(), OsConstants.F_OK); } catch (ErrnoException e) { // Ignore. Could fail if the metadata was already updated. } if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) { Log.e(TAG, "Failed to end rename object"); } if (!success) { return MtpConstants.RESPONSE_GENERAL_ERROR; } updateMediaStore(mContext, oldPath.toFile()); updateMediaStore(mContext, newPath.toFile()); return MtpConstants.RESPONSE_OK; } @VisibleForNative 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; } @VisibleForNative 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; } obj = mManager.getObject(objId); if (!success || obj == null) return; Path path = newParentObj.getPath().resolve(name); Path oldPath = oldParentObj.getPath().resolve(name); updateMediaStore(mContext, oldPath.toFile()); updateMediaStore(mContext, path.toFile()); } @VisibleForNative 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); } @VisibleForNative 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; } updateMediaStore(mContext, obj.getPath().toFile()); } private static void updateMediaStore(@NonNull Context context, @NonNull File file) { final ContentResolver resolver = context.getContentResolver(); // For file, check whether the file name is .nomedia or not. // If yes, scan the parent directory to update all files in the directory. if (!file.isDirectory() && file.getName().toLowerCase(Locale.ROOT).endsWith(NO_MEDIA)) { MediaStore.scanFile(resolver, file.getParentFile()); } else { MediaStore.scanFile(resolver, file); } } @VisibleForNative private int setObjectProperty(int handle, int property, long intValue, String stringValue) { switch (property) { case MtpConstants.PROPERTY_OBJECT_FILE_NAME: return renameFile(handle, stringValue); default: return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED; } } @VisibleForNative private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) { int length; String value; switch (property) { case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: // writable string properties kept in shared preferences value = mDeviceProperties.getString(Integer.toString(property), ""); length = value.length(); if (length > 255) { length = 255; } value.getChars(0, length, outStringValue, 0); outStringValue[length] = 0; return MtpConstants.RESPONSE_OK; case MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO: value = mHostType; length = value.length(); if (length > 255) { length = 255; } 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 // TODO(b/147721765): Add support for foldables/multi-display devices. 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); 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; case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL: outIntValue[0] = mBatteryLevel; outIntValue[1] = mBatteryScale; return MtpConstants.RESPONSE_OK; default: return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; } } @VisibleForNative private int setDeviceProperty(int property, long intValue, String stringValue) { switch (property) { case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: // writable string properties kept in shared prefs SharedPreferences.Editor e = mDeviceProperties.edit(); e.putString(Integer.toString(property), stringValue); return (e.commit() ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR); case MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO: mHostType = stringValue; Log.d(TAG, "setDeviceProperty." + Integer.toHexString(property) + "=" + stringValue); if (stringValue.startsWith("Android/")) { mSkipThumbForHost = true; } else if (stringValue.startsWith("Windows/")) { mHostIsWindows = true; } return MtpConstants.RESPONSE_OK; } return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; } @VisibleForNative private boolean getObjectInfo(int handle, int[] outStorageFormatParent, char[] outName, long[] outCreatedModified) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { 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; } @VisibleForNative private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } 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; } @VisibleForNative private int openFilePath(String path, boolean transcode) { Uri uri = MediaStore.scanFile(mContext.getContentResolver(), new File(path)); if (uri == null) { Log.i(TAG, "Failed to obtain URI for openFile with transcode support: " + path); return -1; } try { Log.i(TAG, "openFile with transcode support: " + path); Bundle bundle = new Bundle(); if (transcode) { bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, new ApplicationMediaCapabilities.Builder().addUnsupportedVideoMimeType( MediaFormat.MIMETYPE_VIDEO_HEVC).build()); } else { bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES, new ApplicationMediaCapabilities.Builder().addSupportedVideoMimeType( MediaFormat.MIMETYPE_VIDEO_HEVC).build()); } return mMediaProvider.openTypedAssetFileDescriptor(uri, "*/*", bundle) .getParcelFileDescriptor().detachFd(); } catch (RemoteException | FileNotFoundException e) { Log.w(TAG, "Failed to openFile with transcode support: " + path, e); return -1; } } private int getObjectFormat(int handle) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return -1; } return obj.getFormat(); } private byte[] getThumbnailProcess(String path, Bitmap bitmap) { try { if (bitmap == null) { Log.d(TAG, "getThumbnailProcess: Fail to generate thumbnail. Probably unsupported or corrupted image"); return null; } ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteStream); if (byteStream.size() > MAX_THUMB_SIZE) { Log.w(TAG, "getThumbnailProcess: size=" + byteStream.size()); return null; } byte[] byteArray = byteStream.toByteArray(); return byteArray; } catch (OutOfMemoryError oomEx) { Log.w(TAG, "OutOfMemoryError:" + oomEx); } return null; } @VisibleForNative @VisibleForTesting public boolean getThumbnailInfo(int handle, long[] outLongs) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return false; } String path = obj.getPath().toString(); switch (obj.getFormat()) { case MtpConstants.FORMAT_HEIF: case MtpConstants.FORMAT_EXIF_JPEG: case MtpConstants.FORMAT_JFIF: try { ExifInterface exif = new ExifInterface(path); long[] thumbOffsetAndSize = exif.getThumbnailRange(); outLongs[0] = thumbOffsetAndSize != null ? thumbOffsetAndSize[1] : 0; outLongs[1] = exif.getAttributeInt(ExifInterface.TAG_PIXEL_X_DIMENSION, 0); outLongs[2] = exif.getAttributeInt(ExifInterface.TAG_PIXEL_Y_DIMENSION, 0); if (mSkipThumbForHost) { Log.d(TAG, "getThumbnailInfo: Skip runtime thumbnail."); return true; } if (exif.getThumbnailRange() != null) { if ((outLongs[0] == 0) || (outLongs[1] == 0) || (outLongs[2] == 0)) { Log.d(TAG, "getThumbnailInfo: check thumb info:" + thumbOffsetAndSize[0] + "," + thumbOffsetAndSize[1] + "," + outLongs[1] + "," + outLongs[2]); } return true; } } catch (IOException e) { // ignore and fall through } // Note: above formats will fall through and go on below thumbnail generation if Exif processing fails case MtpConstants.FORMAT_PNG: case MtpConstants.FORMAT_GIF: case MtpConstants.FORMAT_BMP: outLongs[0] = MAX_THUMB_SIZE; // only non-zero Width & Height needed. Actual size will be retrieved upon getThumbnailData by Host outLongs[1] = 320; outLongs[2] = 240; return true; } return false; } @VisibleForNative @VisibleForTesting public byte[] getThumbnailData(int handle) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return null; } String path = obj.getPath().toString(); switch (obj.getFormat()) { case MtpConstants.FORMAT_HEIF: case MtpConstants.FORMAT_EXIF_JPEG: case MtpConstants.FORMAT_JFIF: try { ExifInterface exif = new ExifInterface(path); if (mSkipThumbForHost) { Log.d(TAG, "getThumbnailData: Skip runtime thumbnail."); return exif.getThumbnail(); } if (exif.getThumbnailRange() != null) return exif.getThumbnail(); } catch (IOException e) { // ignore and fall through } // Note: above formats will fall through and go on below thumbnail generation if Exif processing fails case MtpConstants.FORMAT_PNG: case MtpConstants.FORMAT_GIF: case MtpConstants.FORMAT_BMP: { Bitmap bitmap = ThumbnailUtils.createImageThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND); byte[] byteArray = getThumbnailProcess(path, bitmap); return byteArray; } } return null; } @VisibleForNative private int beginDeleteObject(int handle) { MtpStorageManager.MtpObject obj = mManager.getObject(handle); if (obj == null) { return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; } if (!mManager.beginRemoveObject(obj)) { return MtpConstants.RESPONSE_GENERAL_ERROR; } return MtpConstants.RESPONSE_OK; } @VisibleForNative 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, obj.getPath(), obj.isDir()); } private void deleteFromMedia(MtpStorageManager.MtpObject obj, Path path, boolean isDir) { final Uri objectsUri = MediaStore.Files.getContentUri(obj.getVolumeName()); try { // Delete the object(s) from MediaProvider, but ignore errors. if (isDir) { // recursive case - delete all children first mMediaProvider.delete(objectsUri, // 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() + "/"}); } String[] whereArgs = new String[]{path.toString()}; if (mMediaProvider.delete(objectsUri, PATH_WHERE, whereArgs) == 0) { Log.i(TAG, "MediaProvider didn't delete " + path); } updateMediaStore(mContext, path.toFile()); } catch (Exception e) { Log.d(TAG, "Failed to delete " + path + " from MediaProvider"); } } @VisibleForNative private int[] getObjectReferences(int handle) { return null; } @VisibleForNative private int setObjectReferences(int handle, int[] references) { return MtpConstants.RESPONSE_OPERATION_NOT_SUPPORTED; } @VisibleForNative private long mNativeContext; private native final void native_setup(); private native final void native_finalize(); }