Add MtpStorageManager for monitoring filesystem events
MtpStorageManager keeps track of file information and send notifications for new files. MtpDatabase now uses this instead of MediaProvider for getting object information, although some operations are still reflected into MP. Since MtpStorageManager handles storage ids, remove that field from StorageVolume and VolumeInfo. Clean up a lot of the jni code for MtpDatabase. Bug: 63143623 Test: Test every MtpOperation in a variety of situations on Linux and Windows. Also use the shell to manipulate files. Verify that the cache is consistent throughout, and the operations behave as expected. Verify files created by the shell appear. Test: adb shell am instrument -w android.mtp /android.support.test.runner.AndroidJUnitRunner Change-Id: Id4ea810047b0c323399cd833047733e5daafb30a
This commit is contained in:
@@ -19,7 +19,6 @@ package android.os.storage;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.TrafficStats;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcel;
|
||||
@@ -78,13 +77,11 @@ import java.io.File;
|
||||
public final class StorageVolume implements Parcelable {
|
||||
|
||||
private final String mId;
|
||||
private final int mStorageId;
|
||||
private final File mPath;
|
||||
private final String mDescription;
|
||||
private final boolean mPrimary;
|
||||
private final boolean mRemovable;
|
||||
private final boolean mEmulated;
|
||||
private final long mMtpReserveSize;
|
||||
private final boolean mAllowMassStorage;
|
||||
private final long mMaxFileSize;
|
||||
private final UserHandle mOwner;
|
||||
@@ -121,17 +118,15 @@ public final class StorageVolume implements Parcelable {
|
||||
public static final int STORAGE_ID_PRIMARY = 0x00010001;
|
||||
|
||||
/** {@hide} */
|
||||
public StorageVolume(String id, int storageId, File path, String description, boolean primary,
|
||||
boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage,
|
||||
public StorageVolume(String id, File path, String description, boolean primary,
|
||||
boolean removable, boolean emulated, boolean allowMassStorage,
|
||||
long maxFileSize, UserHandle owner, String fsUuid, String state) {
|
||||
mId = Preconditions.checkNotNull(id);
|
||||
mStorageId = storageId;
|
||||
mPath = Preconditions.checkNotNull(path);
|
||||
mDescription = Preconditions.checkNotNull(description);
|
||||
mPrimary = primary;
|
||||
mRemovable = removable;
|
||||
mEmulated = emulated;
|
||||
mMtpReserveSize = mtpReserveSize;
|
||||
mAllowMassStorage = allowMassStorage;
|
||||
mMaxFileSize = maxFileSize;
|
||||
mOwner = Preconditions.checkNotNull(owner);
|
||||
@@ -141,13 +136,11 @@ public final class StorageVolume implements Parcelable {
|
||||
|
||||
private StorageVolume(Parcel in) {
|
||||
mId = in.readString();
|
||||
mStorageId = in.readInt();
|
||||
mPath = new File(in.readString());
|
||||
mDescription = in.readString();
|
||||
mPrimary = in.readInt() != 0;
|
||||
mRemovable = in.readInt() != 0;
|
||||
mEmulated = in.readInt() != 0;
|
||||
mMtpReserveSize = in.readLong();
|
||||
mAllowMassStorage = in.readInt() != 0;
|
||||
mMaxFileSize = in.readLong();
|
||||
mOwner = in.readParcelable(null);
|
||||
@@ -210,34 +203,6 @@ public final class StorageVolume implements Parcelable {
|
||||
return mEmulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MTP storage ID for the volume.
|
||||
* this is also used for the storage_id column in the media provider.
|
||||
*
|
||||
* @return MTP storage ID
|
||||
* @hide
|
||||
*/
|
||||
public int getStorageId() {
|
||||
return mStorageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of megabytes of space to leave unallocated by MTP.
|
||||
* MTP will subtract this value from the free space it reports back
|
||||
* to the host via GetStorageInfo, and will not allow new files to
|
||||
* be added via MTP if there is less than this amount left free in the storage.
|
||||
* If MTP has dedicated storage this value should be zero, but if MTP is
|
||||
* sharing storage with the rest of the system, set this to a positive value
|
||||
* to ensure that MTP activity does not result in the storage being
|
||||
* too close to full.
|
||||
*
|
||||
* @return MTP reserve space
|
||||
* @hide
|
||||
*/
|
||||
public int getMtpReserveSpace() {
|
||||
return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this volume can be shared via USB mass storage.
|
||||
*
|
||||
@@ -385,13 +350,11 @@ public final class StorageVolume implements Parcelable {
|
||||
pw.println("StorageVolume:");
|
||||
pw.increaseIndent();
|
||||
pw.printPair("mId", mId);
|
||||
pw.printPair("mStorageId", mStorageId);
|
||||
pw.printPair("mPath", mPath);
|
||||
pw.printPair("mDescription", mDescription);
|
||||
pw.printPair("mPrimary", mPrimary);
|
||||
pw.printPair("mRemovable", mRemovable);
|
||||
pw.printPair("mEmulated", mEmulated);
|
||||
pw.printPair("mMtpReserveSize", mMtpReserveSize);
|
||||
pw.printPair("mAllowMassStorage", mAllowMassStorage);
|
||||
pw.printPair("mMaxFileSize", mMaxFileSize);
|
||||
pw.printPair("mOwner", mOwner);
|
||||
@@ -420,13 +383,11 @@ public final class StorageVolume implements Parcelable {
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int flags) {
|
||||
parcel.writeString(mId);
|
||||
parcel.writeInt(mStorageId);
|
||||
parcel.writeString(mPath.toString());
|
||||
parcel.writeString(mDescription);
|
||||
parcel.writeInt(mPrimary ? 1 : 0);
|
||||
parcel.writeInt(mRemovable ? 1 : 0);
|
||||
parcel.writeInt(mEmulated ? 1 : 0);
|
||||
parcel.writeLong(mMtpReserveSize);
|
||||
parcel.writeInt(mAllowMassStorage ? 1 : 0);
|
||||
parcel.writeLong(mMaxFileSize);
|
||||
parcel.writeParcelable(mOwner, flags);
|
||||
|
||||
@@ -343,9 +343,7 @@ public class VolumeInfo implements Parcelable {
|
||||
|
||||
String description = null;
|
||||
String derivedFsUuid = fsUuid;
|
||||
long mtpReserveSize = 0;
|
||||
long maxFileSize = 0;
|
||||
int mtpStorageId = StorageVolume.STORAGE_ID_INVALID;
|
||||
|
||||
if (type == TYPE_EMULATED) {
|
||||
emulated = true;
|
||||
@@ -356,12 +354,6 @@ public class VolumeInfo implements Parcelable {
|
||||
derivedFsUuid = privateVol.fsUuid;
|
||||
}
|
||||
|
||||
if (isPrimary()) {
|
||||
mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
|
||||
}
|
||||
|
||||
mtpReserveSize = storage.getStorageLowBytes(userPath);
|
||||
|
||||
if (ID_EMULATED_INTERNAL.equals(id)) {
|
||||
removable = false;
|
||||
} else {
|
||||
@@ -374,14 +366,6 @@ public class VolumeInfo implements Parcelable {
|
||||
|
||||
description = storage.getBestVolumeDescription(this);
|
||||
|
||||
if (isPrimary()) {
|
||||
mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
|
||||
} else {
|
||||
// Since MediaProvider currently persists this value, we need a
|
||||
// value that is stable over time.
|
||||
mtpStorageId = buildStableMtpStorageId(fsUuid);
|
||||
}
|
||||
|
||||
if ("vfat".equals(fsType)) {
|
||||
maxFileSize = 4294967295L;
|
||||
}
|
||||
@@ -394,8 +378,8 @@ public class VolumeInfo implements Parcelable {
|
||||
description = context.getString(android.R.string.unknownName);
|
||||
}
|
||||
|
||||
return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable,
|
||||
emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId),
|
||||
return new StorageVolume(id, userPath, description, isPrimary(), removable,
|
||||
emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
|
||||
derivedFsUuid, envState);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,15 +63,6 @@ public final class MediaStore {
|
||||
|
||||
private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
|
||||
|
||||
/**
|
||||
* Broadcast Action: A broadcast to indicate the end of an MTP session with the host.
|
||||
* This broadcast is only sent if MTP activity has modified the media database during the
|
||||
* most recent MTP session.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END";
|
||||
|
||||
/**
|
||||
* The method name used by the media scanner and mtp to tell the media provider to
|
||||
* rescan and reclassify that have become unhidden because of renaming folders or
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,22 +23,21 @@ import android.os.RemoteException;
|
||||
import android.provider.MediaStore.Audio;
|
||||
import android.provider.MediaStore.Files;
|
||||
import android.provider.MediaStore.Images;
|
||||
import android.provider.MediaStore.MediaColumns;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* MtpPropertyGroup represents a list of MTP properties.
|
||||
* {@hide}
|
||||
*/
|
||||
class MtpPropertyGroup {
|
||||
|
||||
private static final String TAG = "MtpPropertyGroup";
|
||||
private static final String TAG = MtpPropertyGroup.class.getSimpleName();
|
||||
|
||||
private class Property {
|
||||
// MTP property code
|
||||
int code;
|
||||
// MTP data type
|
||||
int type;
|
||||
// column index for our query
|
||||
int column;
|
||||
int code;
|
||||
int type;
|
||||
int column;
|
||||
|
||||
Property(int code, int type, int column) {
|
||||
this.code = code;
|
||||
@@ -47,32 +46,26 @@ class MtpPropertyGroup {
|
||||
}
|
||||
}
|
||||
|
||||
private final MtpDatabase mDatabase;
|
||||
private final ContentProviderClient mProvider;
|
||||
private final String mVolumeName;
|
||||
private final Uri mUri;
|
||||
|
||||
// list of all properties in this group
|
||||
private final Property[] mProperties;
|
||||
private final Property[] mProperties;
|
||||
|
||||
// list of columns for database query
|
||||
private String[] mColumns;
|
||||
private String[] mColumns;
|
||||
|
||||
private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
|
||||
|
||||
private static final String ID_WHERE = Files.FileColumns._ID + "=?";
|
||||
private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
|
||||
private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE;
|
||||
private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
|
||||
private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE;
|
||||
// constructs a property group for a list of properties
|
||||
public MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName,
|
||||
int[] properties) {
|
||||
mDatabase = database;
|
||||
public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) {
|
||||
mProvider = provider;
|
||||
mVolumeName = volumeName;
|
||||
mUri = Files.getMtpObjectsUri(volumeName);
|
||||
|
||||
int count = properties.length;
|
||||
ArrayList<String> columns = new ArrayList<String>(count);
|
||||
ArrayList<String> columns = new ArrayList<>(count);
|
||||
columns.add(Files.FileColumns._ID);
|
||||
|
||||
mProperties = new Property[count];
|
||||
@@ -90,37 +83,29 @@ class MtpPropertyGroup {
|
||||
String column = null;
|
||||
int type;
|
||||
|
||||
switch (code) {
|
||||
switch (code) {
|
||||
case MtpConstants.PROPERTY_STORAGE_ID:
|
||||
column = Files.FileColumns.STORAGE_ID;
|
||||
type = MtpConstants.TYPE_UINT32;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_OBJECT_FORMAT:
|
||||
column = Files.FileColumns.FORMAT;
|
||||
case MtpConstants.PROPERTY_OBJECT_FORMAT:
|
||||
type = MtpConstants.TYPE_UINT16;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_PROTECTION_STATUS:
|
||||
// protection status is always 0
|
||||
type = MtpConstants.TYPE_UINT16;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_OBJECT_SIZE:
|
||||
column = Files.FileColumns.SIZE;
|
||||
type = MtpConstants.TYPE_UINT64;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
|
||||
column = Files.FileColumns.DATA;
|
||||
type = MtpConstants.TYPE_STR;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_NAME:
|
||||
column = MediaColumns.TITLE;
|
||||
type = MtpConstants.TYPE_STR;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_DATE_MODIFIED:
|
||||
column = Files.FileColumns.DATE_MODIFIED;
|
||||
type = MtpConstants.TYPE_STR;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_DATE_ADDED:
|
||||
column = Files.FileColumns.DATE_ADDED;
|
||||
type = MtpConstants.TYPE_STR;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
|
||||
@@ -128,12 +113,9 @@ class MtpPropertyGroup {
|
||||
type = MtpConstants.TYPE_STR;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_PARENT_OBJECT:
|
||||
column = Files.FileColumns.PARENT;
|
||||
type = MtpConstants.TYPE_UINT32;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_PERSISTENT_UID:
|
||||
// PUID is concatenation of storageID and object handle
|
||||
column = Files.FileColumns.STORAGE_ID;
|
||||
type = MtpConstants.TYPE_UINT128;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_DURATION:
|
||||
@@ -145,7 +127,6 @@ class MtpPropertyGroup {
|
||||
type = MtpConstants.TYPE_UINT16;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_DISPLAY_NAME:
|
||||
column = MediaColumns.DISPLAY_NAME;
|
||||
type = MtpConstants.TYPE_STR;
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ARTIST:
|
||||
@@ -195,40 +176,19 @@ class MtpPropertyGroup {
|
||||
}
|
||||
}
|
||||
|
||||
private String queryString(int id, String column) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
// for now we are only reading properties from the "objects" table
|
||||
c = mProvider.query(mUri,
|
||||
new String [] { Files.FileColumns._ID, column },
|
||||
ID_WHERE, new String[] { Integer.toString(id) }, null, null);
|
||||
if (c != null && c.moveToNext()) {
|
||||
return c.getString(1);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String queryAudio(int id, String column) {
|
||||
private String queryAudio(String path, String column) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = mProvider.query(Audio.Media.getContentUri(mVolumeName),
|
||||
new String [] { Files.FileColumns._ID, column },
|
||||
ID_WHERE, new String[] { Integer.toString(id) }, null, null);
|
||||
new String [] { column },
|
||||
PATH_WHERE, new String[] {path}, null, null);
|
||||
if (c != null && c.moveToNext()) {
|
||||
return c.getString(1);
|
||||
return c.getString(0);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
return "";
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
@@ -236,21 +196,19 @@ class MtpPropertyGroup {
|
||||
}
|
||||
}
|
||||
|
||||
private String queryGenre(int id) {
|
||||
private String queryGenre(String path) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id);
|
||||
c = mProvider.query(uri,
|
||||
new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME },
|
||||
null, null, null, null);
|
||||
c = mProvider.query(Audio.Genres.getContentUri(mVolumeName),
|
||||
new String [] { Audio.GenresColumns.NAME },
|
||||
PATH_WHERE, new String[] {path}, null, null);
|
||||
if (c != null && c.moveToNext()) {
|
||||
return c.getString(1);
|
||||
return c.getString(0);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "queryGenre exception", e);
|
||||
return null;
|
||||
return "";
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
@@ -258,211 +216,127 @@ class MtpPropertyGroup {
|
||||
}
|
||||
}
|
||||
|
||||
private Long queryLong(int id, String column) {
|
||||
/**
|
||||
* Gets the values of the properties represented by this property group for the given
|
||||
* object and adds them to the given property list.
|
||||
* @return Response_OK if the operation succeeded.
|
||||
*/
|
||||
public int getPropertyList(MtpStorageManager.MtpObject object, MtpPropertyList list) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
// for now we are only reading properties from the "objects" table
|
||||
c = mProvider.query(mUri,
|
||||
new String [] { Files.FileColumns._ID, column },
|
||||
ID_WHERE, new String[] { Integer.toString(id) }, null, null);
|
||||
if (c != null && c.moveToNext()) {
|
||||
return new Long(c.getLong(1));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String nameFromPath(String path) {
|
||||
// extract name from full path
|
||||
int start = 0;
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash >= 0) {
|
||||
start = lastSlash + 1;
|
||||
}
|
||||
int end = path.length();
|
||||
if (end - start > 255) {
|
||||
end = start + 255;
|
||||
}
|
||||
return path.substring(start, end);
|
||||
}
|
||||
|
||||
MtpPropertyList getPropertyList(int handle, int format, int depth) {
|
||||
//Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth);
|
||||
if (depth > 1) {
|
||||
// we only support depth 0 and 1
|
||||
// depth 0: single object, depth 1: immediate children
|
||||
return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
|
||||
}
|
||||
|
||||
String where;
|
||||
String[] whereArgs;
|
||||
if (format == 0) {
|
||||
if (handle == 0xFFFFFFFF) {
|
||||
// select all objects
|
||||
where = null;
|
||||
whereArgs = null;
|
||||
} else {
|
||||
whereArgs = new String[] { Integer.toString(handle) };
|
||||
if (depth == 1) {
|
||||
where = PARENT_WHERE;
|
||||
} else {
|
||||
where = ID_WHERE;
|
||||
int id = object.getId();
|
||||
String path = object.getPath().toString();
|
||||
for (Property property : mProperties) {
|
||||
if (property.column != -1 && c == null) {
|
||||
try {
|
||||
// Look up the entry in MediaProvider only if one of those properties is needed.
|
||||
c = mProvider.query(mUri, mColumns,
|
||||
PATH_WHERE, new String[] {path}, null, null);
|
||||
if (c != null && !c.moveToNext()) {
|
||||
c.close();
|
||||
c = null;
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Mediaprovider lookup failed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (handle == 0xFFFFFFFF) {
|
||||
// select all objects with given format
|
||||
where = FORMAT_WHERE;
|
||||
whereArgs = new String[] { Integer.toString(format) };
|
||||
} else {
|
||||
whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) };
|
||||
if (depth == 1) {
|
||||
where = PARENT_FORMAT_WHERE;
|
||||
} else {
|
||||
where = ID_FORMAT_WHERE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cursor c = null;
|
||||
try {
|
||||
// don't query if not necessary
|
||||
if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) {
|
||||
c = mProvider.query(mUri, mColumns, where, whereArgs, null, null);
|
||||
if (c == null) {
|
||||
return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
|
||||
}
|
||||
}
|
||||
|
||||
int count = (c == null ? 1 : c.getCount());
|
||||
MtpPropertyList result = new MtpPropertyList(count * mProperties.length,
|
||||
MtpConstants.RESPONSE_OK);
|
||||
|
||||
// iterate over all objects in the query
|
||||
for (int objectIndex = 0; objectIndex < count; objectIndex++) {
|
||||
if (c != null) {
|
||||
c.moveToNext();
|
||||
handle = (int)c.getLong(0);
|
||||
}
|
||||
|
||||
// iterate over all properties in the query for the given object
|
||||
for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) {
|
||||
Property property = mProperties[propertyIndex];
|
||||
int propertyCode = property.code;
|
||||
int column = property.column;
|
||||
|
||||
// handle some special cases
|
||||
switch (propertyCode) {
|
||||
case MtpConstants.PROPERTY_PROTECTION_STATUS:
|
||||
// protection status is always 0
|
||||
result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
|
||||
switch (property.code) {
|
||||
case MtpConstants.PROPERTY_PROTECTION_STATUS:
|
||||
// protection status is always 0
|
||||
list.append(id, property.code, property.type, 0);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_NAME:
|
||||
case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
|
||||
case MtpConstants.PROPERTY_DISPLAY_NAME:
|
||||
list.append(id, property.code, object.getName());
|
||||
break;
|
||||
case MtpConstants.PROPERTY_DATE_MODIFIED:
|
||||
case MtpConstants.PROPERTY_DATE_ADDED:
|
||||
// convert from seconds to DateTime
|
||||
list.append(id, property.code,
|
||||
format_date_time(object.getModifiedTime()));
|
||||
break;
|
||||
case MtpConstants.PROPERTY_STORAGE_ID:
|
||||
list.append(id, property.code, property.type, object.getStorageId());
|
||||
break;
|
||||
case MtpConstants.PROPERTY_OBJECT_FORMAT:
|
||||
list.append(id, property.code, property.type, object.getFormat());
|
||||
break;
|
||||
case MtpConstants.PROPERTY_OBJECT_SIZE:
|
||||
list.append(id, property.code, property.type, object.getSize());
|
||||
break;
|
||||
case MtpConstants.PROPERTY_PARENT_OBJECT:
|
||||
list.append(id, property.code, property.type,
|
||||
object.getParent().isRoot() ? 0 : object.getParent().getId());
|
||||
break;
|
||||
case MtpConstants.PROPERTY_PERSISTENT_UID:
|
||||
// The persistent uid must be unique and never reused among all objects,
|
||||
// and remain the same between sessions.
|
||||
long puid = (object.getPath().toString().hashCode() << 32)
|
||||
+ object.getModifiedTime();
|
||||
list.append(id, property.code, property.type, puid);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
|
||||
// release date is stored internally as just the year
|
||||
int year = 0;
|
||||
if (c != null)
|
||||
year = c.getInt(property.column);
|
||||
String dateTime = Integer.toString(year) + "0101T000000";
|
||||
list.append(id, property.code, dateTime);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_TRACK:
|
||||
int track = 0;
|
||||
if (c != null)
|
||||
track = c.getInt(property.column);
|
||||
list.append(id, property.code, MtpConstants.TYPE_UINT16,
|
||||
track % 1000);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ARTIST:
|
||||
list.append(id, property.code,
|
||||
queryAudio(path, Audio.AudioColumns.ARTIST));
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ALBUM_NAME:
|
||||
list.append(id, property.code,
|
||||
queryAudio(path, Audio.AudioColumns.ALBUM));
|
||||
break;
|
||||
case MtpConstants.PROPERTY_GENRE:
|
||||
String genre = queryGenre(path);
|
||||
if (genre != null) {
|
||||
list.append(id, property.code, genre);
|
||||
}
|
||||
break;
|
||||
case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
|
||||
case MtpConstants.PROPERTY_AUDIO_BITRATE:
|
||||
case MtpConstants.PROPERTY_SAMPLE_RATE:
|
||||
// we don't have these in our database, so return 0
|
||||
list.append(id, property.code, MtpConstants.TYPE_UINT32, 0);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_BITRATE_TYPE:
|
||||
case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
|
||||
// we don't have these in our database, so return 0
|
||||
list.append(id, property.code, MtpConstants.TYPE_UINT16, 0);
|
||||
break;
|
||||
default:
|
||||
switch(property.type) {
|
||||
case MtpConstants.TYPE_UNDEFINED:
|
||||
list.append(id, property.code, property.type, 0);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
|
||||
// special case - need to extract file name from full path
|
||||
String value = c.getString(column);
|
||||
if (value != null) {
|
||||
result.append(handle, propertyCode, nameFromPath(value));
|
||||
} else {
|
||||
result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
|
||||
}
|
||||
break;
|
||||
case MtpConstants.PROPERTY_NAME:
|
||||
// first try title
|
||||
String name = c.getString(column);
|
||||
// then try name
|
||||
if (name == null) {
|
||||
name = queryString(handle, Audio.PlaylistsColumns.NAME);
|
||||
}
|
||||
// if title and name fail, extract name from full path
|
||||
if (name == null) {
|
||||
name = queryString(handle, Files.FileColumns.DATA);
|
||||
if (name != null) {
|
||||
name = nameFromPath(name);
|
||||
}
|
||||
}
|
||||
if (name != null) {
|
||||
result.append(handle, propertyCode, name);
|
||||
} else {
|
||||
result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
|
||||
}
|
||||
break;
|
||||
case MtpConstants.PROPERTY_DATE_MODIFIED:
|
||||
case MtpConstants.PROPERTY_DATE_ADDED:
|
||||
// convert from seconds to DateTime
|
||||
result.append(handle, propertyCode, format_date_time(c.getInt(column)));
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
|
||||
// release date is stored internally as just the year
|
||||
int year = c.getInt(column);
|
||||
String dateTime = Integer.toString(year) + "0101T000000";
|
||||
result.append(handle, propertyCode, dateTime);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_PERSISTENT_UID:
|
||||
// PUID is concatenation of storageID and object handle
|
||||
long puid = c.getLong(column);
|
||||
puid <<= 32;
|
||||
puid += handle;
|
||||
result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_TRACK:
|
||||
result.append(handle, propertyCode, MtpConstants.TYPE_UINT16,
|
||||
c.getInt(column) % 1000);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ARTIST:
|
||||
result.append(handle, propertyCode,
|
||||
queryAudio(handle, Audio.AudioColumns.ARTIST));
|
||||
break;
|
||||
case MtpConstants.PROPERTY_ALBUM_NAME:
|
||||
result.append(handle, propertyCode,
|
||||
queryAudio(handle, Audio.AudioColumns.ALBUM));
|
||||
break;
|
||||
case MtpConstants.PROPERTY_GENRE:
|
||||
String genre = queryGenre(handle);
|
||||
if (genre != null) {
|
||||
result.append(handle, propertyCode, genre);
|
||||
} else {
|
||||
result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
|
||||
}
|
||||
break;
|
||||
case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
|
||||
case MtpConstants.PROPERTY_AUDIO_BITRATE:
|
||||
case MtpConstants.PROPERTY_SAMPLE_RATE:
|
||||
// we don't have these in our database, so return 0
|
||||
result.append(handle, propertyCode, MtpConstants.TYPE_UINT32, 0);
|
||||
break;
|
||||
case MtpConstants.PROPERTY_BITRATE_TYPE:
|
||||
case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
|
||||
// we don't have these in our database, so return 0
|
||||
result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
|
||||
case MtpConstants.TYPE_STR:
|
||||
String value = "";
|
||||
if (c != null)
|
||||
value = c.getString(property.column);
|
||||
list.append(id, property.code, value);
|
||||
break;
|
||||
default:
|
||||
if (property.type == MtpConstants.TYPE_STR) {
|
||||
result.append(handle, propertyCode, c.getString(column));
|
||||
} else if (property.type == MtpConstants.TYPE_UNDEFINED) {
|
||||
result.append(handle, propertyCode, property.type, 0);
|
||||
} else {
|
||||
result.append(handle, propertyCode, property.type,
|
||||
c.getLong(column));
|
||||
}
|
||||
break;
|
||||
long longValue = 0L;
|
||||
if (c != null)
|
||||
longValue = c.getLong(property.column);
|
||||
list.append(id, property.code, property.type, longValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (RemoteException e) {
|
||||
return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
// impossible to get here, so no return statement
|
||||
if (c != null)
|
||||
c.close();
|
||||
return MtpConstants.RESPONSE_OK;
|
||||
}
|
||||
|
||||
private native String format_date_time(long seconds);
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package android.mtp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encapsulates the ObjectPropList dataset used by the GetObjectPropList command.
|
||||
* The fields of this class are read by JNI code in android_media_MtpDatabase.cpp
|
||||
@@ -23,56 +26,70 @@ package android.mtp;
|
||||
|
||||
class MtpPropertyList {
|
||||
|
||||
// number of results returned
|
||||
private int mCount;
|
||||
// maximum number of results
|
||||
private final int mMaxCount;
|
||||
// result code for GetObjectPropList
|
||||
public int mResult;
|
||||
// list of object handles (first field in quadruplet)
|
||||
public final int[] mObjectHandles;
|
||||
// list of object propery codes (second field in quadruplet)
|
||||
public final int[] mPropertyCodes;
|
||||
private List<Integer> mObjectHandles;
|
||||
// list of object property codes (second field in quadruplet)
|
||||
private List<Integer> mPropertyCodes;
|
||||
// list of data type codes (third field in quadruplet)
|
||||
public final int[] mDataTypes;
|
||||
private List<Integer> mDataTypes;
|
||||
// list of long int property values (fourth field in quadruplet, when value is integer type)
|
||||
public long[] mLongValues;
|
||||
private List<Long> mLongValues;
|
||||
// list of long int property values (fourth field in quadruplet, when value is string type)
|
||||
public String[] mStringValues;
|
||||
private List<String> mStringValues;
|
||||
|
||||
// constructor only called from MtpDatabase
|
||||
public MtpPropertyList(int maxCount, int result) {
|
||||
mMaxCount = maxCount;
|
||||
mResult = result;
|
||||
mObjectHandles = new int[maxCount];
|
||||
mPropertyCodes = new int[maxCount];
|
||||
mDataTypes = new int[maxCount];
|
||||
// mLongValues and mStringValues are created lazily since both might not be necessary
|
||||
// Return value of this operation
|
||||
private int mCode;
|
||||
|
||||
public MtpPropertyList(int code) {
|
||||
mCode = code;
|
||||
mObjectHandles = new ArrayList<>();
|
||||
mPropertyCodes = new ArrayList<>();
|
||||
mDataTypes = new ArrayList<>();
|
||||
mLongValues = new ArrayList<>();
|
||||
mStringValues = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void append(int handle, int property, int type, long value) {
|
||||
int index = mCount++;
|
||||
if (mLongValues == null) {
|
||||
mLongValues = new long[mMaxCount];
|
||||
}
|
||||
mObjectHandles[index] = handle;
|
||||
mPropertyCodes[index] = property;
|
||||
mDataTypes[index] = type;
|
||||
mLongValues[index] = value;
|
||||
mObjectHandles.add(handle);
|
||||
mPropertyCodes.add(property);
|
||||
mDataTypes.add(type);
|
||||
mLongValues.add(value);
|
||||
mStringValues.add(null);
|
||||
}
|
||||
|
||||
public void append(int handle, int property, String value) {
|
||||
int index = mCount++;
|
||||
if (mStringValues == null) {
|
||||
mStringValues = new String[mMaxCount];
|
||||
}
|
||||
mObjectHandles[index] = handle;
|
||||
mPropertyCodes[index] = property;
|
||||
mDataTypes[index] = MtpConstants.TYPE_STR;
|
||||
mStringValues[index] = value;
|
||||
mObjectHandles.add(handle);
|
||||
mPropertyCodes.add(property);
|
||||
mDataTypes.add(MtpConstants.TYPE_STR);
|
||||
mStringValues.add(value);
|
||||
mLongValues.add(0L);
|
||||
}
|
||||
|
||||
public void setResult(int result) {
|
||||
mResult = result;
|
||||
public int getCode() {
|
||||
return mCode;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return mObjectHandles.size();
|
||||
}
|
||||
|
||||
public int[] getObjectHandles() {
|
||||
return mObjectHandles.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
|
||||
public int[] getPropertyCodes() {
|
||||
return mPropertyCodes.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
|
||||
public int[] getDataTypes() {
|
||||
return mDataTypes.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
|
||||
public long[] getLongValues() {
|
||||
return mLongValues.stream().mapToLong(Long::longValue).toArray();
|
||||
}
|
||||
|
||||
public String[] getStringValues() {
|
||||
return mStringValues.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +31,13 @@ public class MtpStorage {
|
||||
private final int mStorageId;
|
||||
private final String mPath;
|
||||
private final String mDescription;
|
||||
private final long mReserveSpace;
|
||||
private final boolean mRemovable;
|
||||
private final long mMaxFileSize;
|
||||
|
||||
public MtpStorage(StorageVolume volume, Context context) {
|
||||
mStorageId = volume.getStorageId();
|
||||
public MtpStorage(StorageVolume volume, int storageId) {
|
||||
mStorageId = storageId;
|
||||
mPath = volume.getPath();
|
||||
mDescription = volume.getDescription(context);
|
||||
mReserveSpace = volume.getMtpReserveSpace() * 1024L * 1024L;
|
||||
mDescription = volume.getDescription(null);
|
||||
mRemovable = volume.isRemovable();
|
||||
mMaxFileSize = volume.getMaxFileSize();
|
||||
}
|
||||
@@ -71,16 +69,6 @@ public class MtpStorage {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of space to reserve on the storage file system.
|
||||
* This can be set to a non-zero value to prevent MTP from filling up the entire storage.
|
||||
*
|
||||
* @return reserved space in bytes.
|
||||
*/
|
||||
public final long getReserveSpace() {
|
||||
return mReserveSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the storage is removable.
|
||||
*
|
||||
|
||||
1210
media/java/android/mtp/MtpStorageManager.java
Normal file
1210
media/java/android/mtp/MtpStorageManager.java
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,6 @@ static jfieldID field_MtpServer_nativeContext;
|
||||
static jfieldID field_MtpStorage_storageId;
|
||||
static jfieldID field_MtpStorage_path;
|
||||
static jfieldID field_MtpStorage_description;
|
||||
static jfieldID field_MtpStorage_reserveSpace;
|
||||
static jfieldID field_MtpStorage_removable;
|
||||
static jfieldID field_MtpStorage_maxFileSize;
|
||||
|
||||
@@ -50,7 +49,7 @@ static Mutex sMutex;
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// in android_mtp_MtpDatabase.cpp
|
||||
extern MtpDatabase* getMtpDatabase(JNIEnv *env, jobject database);
|
||||
extern IMtpDatabase* getMtpDatabase(JNIEnv *env, jobject database);
|
||||
|
||||
static inline MtpServer* getMtpServer(JNIEnv *env, jobject thiz) {
|
||||
return (MtpServer*)env->GetLongField(thiz, field_MtpServer_nativeContext);
|
||||
@@ -162,7 +161,6 @@ android_mtp_MtpServer_add_storage(JNIEnv *env, jobject thiz, jobject jstorage)
|
||||
jint storageID = env->GetIntField(jstorage, field_MtpStorage_storageId);
|
||||
jstring path = (jstring)env->GetObjectField(jstorage, field_MtpStorage_path);
|
||||
jstring description = (jstring)env->GetObjectField(jstorage, field_MtpStorage_description);
|
||||
jlong reserveSpace = env->GetLongField(jstorage, field_MtpStorage_reserveSpace);
|
||||
jboolean removable = env->GetBooleanField(jstorage, field_MtpStorage_removable);
|
||||
jlong maxFileSize = env->GetLongField(jstorage, field_MtpStorage_maxFileSize);
|
||||
|
||||
@@ -171,7 +169,7 @@ android_mtp_MtpServer_add_storage(JNIEnv *env, jobject thiz, jobject jstorage)
|
||||
const char *descriptionStr = env->GetStringUTFChars(description, NULL);
|
||||
if (descriptionStr != NULL) {
|
||||
MtpStorage* storage = new MtpStorage(storageID, pathStr, descriptionStr,
|
||||
reserveSpace, removable, maxFileSize);
|
||||
removable, maxFileSize);
|
||||
server->addStorage(storage);
|
||||
env->ReleaseStringUTFChars(path, pathStr);
|
||||
env->ReleaseStringUTFChars(description, descriptionStr);
|
||||
@@ -241,11 +239,6 @@ int register_android_mtp_MtpServer(JNIEnv *env)
|
||||
ALOGE("Can't find MtpStorage.mDescription");
|
||||
return -1;
|
||||
}
|
||||
field_MtpStorage_reserveSpace = env->GetFieldID(clazz, "mReserveSpace", "J");
|
||||
if (field_MtpStorage_reserveSpace == NULL) {
|
||||
ALOGE("Can't find MtpStorage.mReserveSpace");
|
||||
return -1;
|
||||
}
|
||||
field_MtpStorage_removable = env->GetFieldID(clazz, "mRemovable", "Z");
|
||||
if (field_MtpStorage_removable == NULL) {
|
||||
ALOGE("Can't find MtpStorage.mRemovable");
|
||||
|
||||
12
media/tests/MtpTests/Android.mk
Normal file
12
media/tests/MtpTests/Android.mk
Normal file
@@ -0,0 +1,12 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE_TAGS := tests
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-subdir-java-files)
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
|
||||
|
||||
LOCAL_PACKAGE_NAME := MtpTests
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
31
media/tests/MtpTests/AndroidManifest.xml
Normal file
31
media/tests/MtpTests/AndroidManifest.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2008 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="android.mtp" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="android.mtp"
|
||||
android:label="MtpTests"/>
|
||||
</manifest>
|
||||
15
media/tests/MtpTests/AndroidTest.xml
Normal file
15
media/tests/MtpTests/AndroidTest.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<configuration description="Runs sample instrumentation test.">
|
||||
<target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
|
||||
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
|
||||
<option name="test-file-name" value="MtpTests.apk"/>
|
||||
</target_preparer>
|
||||
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
|
||||
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
|
||||
<option name="test-suite-tag" value="apct"/>
|
||||
<option name="test-tag" value="MtpTests"/>
|
||||
|
||||
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
|
||||
<option name="package" value="android.mtp"/>
|
||||
<option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
|
||||
</test>
|
||||
</configuration>
|
||||
1657
media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java
Normal file
1657
media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2691,15 +2691,14 @@ class StorageManagerService extends IStorageManager.Stub implements Watchdog.Mon
|
||||
final boolean primary = true;
|
||||
final boolean removable = primaryPhysical;
|
||||
final boolean emulated = !primaryPhysical;
|
||||
final long mtpReserveSize = 0L;
|
||||
final boolean allowMassStorage = false;
|
||||
final long maxFileSize = 0L;
|
||||
final UserHandle owner = new UserHandle(userId);
|
||||
final String uuid = null;
|
||||
final String state = Environment.MEDIA_REMOVED;
|
||||
|
||||
res.add(0, new StorageVolume(id, StorageVolume.STORAGE_ID_INVALID, path,
|
||||
description, primary, removable, emulated, mtpReserveSize,
|
||||
res.add(0, new StorageVolume(id, path,
|
||||
description, primary, removable, emulated,
|
||||
allowMassStorage, maxFileSize, owner, uuid, state));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user