Kevin Rocard f0fbd6439e Limit transcoding size workaround to windows
Test: Connect android to windows through usbc.
      Enable mtp, then enable the transcoding option.
      Copy H265 video from the camera folder to windows' desktop.
      Transcoded H264 video plays correctly in WMPcorrectly.

Bug: 184117074
Bug: 190422448
Change-Id: I17264f0ee3e742315569d4c50817b1da7f3f2eb4
2021-11-26 17:05:39 +00:00

1004 lines
39 KiB
Java
Executable File

/*
* 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<String, MtpStorage> mStorageMap = new HashMap<>();
// cached property groups for single properties
private final SparseArray<MtpPropertyGroup> mPropertyGroupsByProperty = new SparseArray<>();
// cached property groups for all properties for a given format
private final SparseArray<MtpPropertyGroup> 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<MtpStorageManager.MtpObject> 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<MtpStorageManager.MtpObject> 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<MtpStorageManager.MtpObject> 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();
}