Files
frameworks_base/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
Jeff Sharkey 91e3cd4c6c More robust MTP-to-MIME-type mappings.
Recently in I830717428e72ac37c5ecd1f23d915aa878ef3744, we greatly
improved the underlying file-extension-to-MIME-type mappings defined
in libcore and used across the OS.

Instead of maintaining divergent mappings here in MediaFile, this
change delegates all file extension logic down to libcore, and
standardizes all MediaScanner internals on using MIME types.  To
register new file types in the future:

1. Add the MIME-to-extension registration in libcore.
2. Add the MIME-to-MTP mapping here in MediaFile.

This change also ensures that unknown MIME types are surfaced
across MTP, using constants like FORMAT_UNDEFINED_AUDIO for audio/*
until an explicit format is defined.

We now surface WMA/WMV file formats, even if the device can't
natively play them back, since we still want to offer the ability
for users to copy them around, and the user may have a third-party
app capable of playing them.

Keeps @UnsupportedAppUsage intact for now.

Bug: 111268862, 112162449
Test: atest frameworks/base/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/MediaFileTest.java
Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore*
Change-Id: I2f6a5411bc215f776f00e0f9a4b7d825b10b377d
2018-08-28 13:02:35 -06:00

922 lines
35 KiB
Java

/*
* Copyright (C) 2015 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 com.android.mtp;
import static com.android.mtp.MtpDatabaseConstants.*;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.media.MediaFile;
import android.mtp.MtpConstants;
import android.mtp.MtpObjectInfo;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.MetadataReader;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* Database for MTP objects.
* The object handle which is identifier for object in MTP protocol is not stable over sessions.
* When we resume the process, we need to remap our document ID with MTP's object handle.
*
* If the remote MTP device is backed by typical file system, the file name
* is unique among files in a directory. However, MTP protocol itself does
* not guarantee the uniqueness of name so we cannot use fullpath as ID.
*
* Instead of fullpath, we use artificial ID generated by MtpDatabase itself. The database object
* remembers the map of document ID and object handle, and remaps new object handle with document ID
* by comparing the directory structure and object name.
*
* To start putting documents into the database, the client needs to call
* {@link Mapper#startAddingDocuments(String)} with the parent document ID. Also it
* needs to call {@link Mapper#stopAddingDocuments(String)} after putting all child
* documents to the database. (All explanations are same for root documents)
*
* database.getMapper().startAddingDocuments();
* database.getMapper().putChildDocuments();
* database.getMapper().stopAddingDocuments();
*
* To update the existing documents, the client code can repeat to call the three methods again.
* The newly added rows update corresponding existing rows that have same MTP identifier like
* objectHandle.
*
* The client can call putChildDocuments multiple times to add documents by chunk, but it needs to
* put all documents under the parent before calling stopAddingChildDocuments. Otherwise missing
* documents are regarded as deleted, and will be removed from the database.
*
* If the client calls clearMtpIdentifier(), it clears MTP identifier in the database. In this case,
* the database tries to find corresponding rows by using document's name instead of MTP identifier
* at the next update cycle.
*
* TODO: Improve performance by SQL optimization.
*/
class MtpDatabase {
private final SQLiteDatabase mDatabase;
private final Mapper mMapper;
SQLiteDatabase getSQLiteDatabase() {
return mDatabase;
}
MtpDatabase(Context context, int flags) {
final OpenHelper helper = new OpenHelper(context, flags);
mDatabase = helper.getWritableDatabase();
mMapper = new Mapper(this);
}
void close() {
mDatabase.close();
}
/**
* Returns operations for mapping.
* @return Mapping operations.
*/
Mapper getMapper() {
return mMapper;
}
/**
* Queries roots information.
* @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}.
* @return Database cursor.
*/
Cursor queryRoots(Resources resources, String[] columnNames) {
final String selection =
COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?";
final Cursor deviceCursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(COLUMN_DEVICE_ID),
selection,
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE),
COLUMN_DEVICE_ID,
null,
null,
null);
try {
final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(JOIN_ROOTS);
builder.setProjectionMap(COLUMN_MAP_ROOTS);
final MatrixCursor result = new MatrixCursor(columnNames);
final ContentValues values = new ContentValues();
while (deviceCursor.moveToNext()) {
final int deviceId = deviceCursor.getInt(0);
final Cursor storageCursor = builder.query(
mDatabase,
columnNames,
selection + " AND " + COLUMN_DEVICE_ID + " = ?",
strings(ROW_STATE_VALID,
ROW_STATE_INVALIDATED,
DOCUMENT_TYPE_STORAGE,
deviceId),
null,
null,
null);
try {
values.clear();
try (final Cursor deviceRoot = builder.query(
mDatabase,
columnNames,
selection + " AND " + COLUMN_DEVICE_ID + " = ?",
strings(ROW_STATE_VALID,
ROW_STATE_INVALIDATED,
DOCUMENT_TYPE_DEVICE,
deviceId),
null,
null,
null)) {
deviceRoot.moveToNext();
DatabaseUtils.cursorRowToContentValues(deviceRoot, values);
}
if (storageCursor.getCount() != 0) {
long capacityBytes = 0;
long availableBytes = 0;
final int capacityIndex =
storageCursor.getColumnIndex(Root.COLUMN_CAPACITY_BYTES);
final int availableIndex =
storageCursor.getColumnIndex(Root.COLUMN_AVAILABLE_BYTES);
while (storageCursor.moveToNext()) {
// If requested columnNames does not include COLUMN_XXX_BYTES, we
// don't calculate corresponding values.
if (capacityIndex != -1) {
capacityBytes += storageCursor.getLong(capacityIndex);
}
if (availableIndex != -1) {
availableBytes += storageCursor.getLong(availableIndex);
}
}
values.put(Root.COLUMN_CAPACITY_BYTES, capacityBytes);
values.put(Root.COLUMN_AVAILABLE_BYTES, availableBytes);
} else {
values.putNull(Root.COLUMN_CAPACITY_BYTES);
values.putNull(Root.COLUMN_AVAILABLE_BYTES);
}
if (storageCursor.getCount() == 1 && values.containsKey(Root.COLUMN_TITLE)) {
storageCursor.moveToFirst();
// Add storage name to device name if we have only 1 storage.
values.put(
Root.COLUMN_TITLE,
resources.getString(
R.string.root_name,
values.getAsString(Root.COLUMN_TITLE),
storageCursor.getString(
storageCursor.getColumnIndex(Root.COLUMN_TITLE))));
}
} finally {
storageCursor.close();
}
putValuesToCursor(values, result);
}
return result;
} finally {
deviceCursor.close();
}
}
/**
* Queries root documents information.
* @param columnNames Column names defined in
* {@link android.provider.DocumentsContract.Document}.
* @return Database cursor.
*/
@VisibleForTesting
Cursor queryRootDocuments(String[] columnNames) {
return mDatabase.query(
TABLE_DOCUMENTS,
columnNames,
COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE),
null,
null,
null);
}
/**
* Queries documents information.
* @param columnNames Column names defined in
* {@link android.provider.DocumentsContract.Document}.
* @return Database cursor.
*/
Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
return mDatabase.query(
TABLE_DOCUMENTS,
columnNames,
COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_PARENT_DOCUMENT_ID + " = ?",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, parentDocumentId),
null,
null,
null);
}
/**
* Returns document IDs of storages under the given device document.
*
* @param documentId Document ID that points a device.
* @return Storage document IDs.
* @throws FileNotFoundException The given document ID is not registered in database.
*/
String[] getStorageDocumentIds(String documentId)
throws FileNotFoundException {
Preconditions.checkArgument(createIdentifier(documentId).mDocumentType ==
DOCUMENT_TYPE_DEVICE);
// Check if the parent document is device that has single storage.
try (final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
COLUMN_ROW_STATE + " IN (?, ?) AND " +
COLUMN_PARENT_DOCUMENT_ID + " = ? AND " +
COLUMN_DOCUMENT_TYPE + " = ?",
strings(ROW_STATE_VALID,
ROW_STATE_INVALIDATED,
documentId,
DOCUMENT_TYPE_STORAGE),
null,
null,
null)) {
final String[] ids = new String[cursor.getCount()];
for (int i = 0; cursor.moveToNext(); i++) {
ids[i] = cursor.getString(0);
}
return ids;
}
}
/**
* Queries a single document.
* @param documentId
* @param projection
* @return Database cursor.
*/
Cursor queryDocument(String documentId, String[] projection) {
return mDatabase.query(
TABLE_DOCUMENTS,
projection,
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1");
}
@Nullable String getDocumentIdForDevice(int deviceId) {
final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
COLUMN_DOCUMENT_TYPE + " = ? AND " + COLUMN_DEVICE_ID + " = ?",
strings(DOCUMENT_TYPE_DEVICE, deviceId),
null,
null,
null,
"1");
try {
if (cursor.moveToNext()) {
return cursor.getString(0);
} else {
return null;
}
} finally {
cursor.close();
}
}
/**
* Obtains parent identifier.
* @param documentId
* @return parent identifier.
* @throws FileNotFoundException
*/
Identifier getParentIdentifier(String documentId) throws FileNotFoundException {
final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(COLUMN_PARENT_DOCUMENT_ID),
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1");
try {
if (cursor.moveToNext()) {
return createIdentifier(cursor.getString(0));
} else {
throw new FileNotFoundException("Cannot find a row having ID = " + documentId);
}
} finally {
cursor.close();
}
}
String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
try (final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
COLUMN_DEVICE_ID + " = ? AND " + COLUMN_DOCUMENT_TYPE + " = ? AND " +
COLUMN_ROW_STATE + " != ?",
strings(deviceId, DOCUMENT_TYPE_DEVICE, ROW_STATE_DISCONNECTED),
null,
null,
null,
"1")) {
if (cursor.getCount() > 0) {
cursor.moveToNext();
return cursor.getString(0);
} else {
throw new FileNotFoundException("The device ID not found: " + deviceId);
}
}
}
/**
* Adds new document under the parent.
* The method does not affect invalidated and pending documents because we know the document is
* newly added and never mapped with existing ones.
* @param parentDocumentId
* @param info
* @param size Object size. info#getCompressedSize() will be ignored because it does not contain
* object size more than 4GB.
* @return Document ID of added document.
*/
String putNewDocument(
int deviceId, String parentDocumentId, int[] operationsSupported, MtpObjectInfo info,
long size) {
final ContentValues values = new ContentValues();
getObjectDocumentValues(
values, deviceId, parentDocumentId, operationsSupported, info, size);
mDatabase.beginTransaction();
try {
final long id = mDatabase.insert(TABLE_DOCUMENTS, null, values);
mDatabase.setTransactionSuccessful();
return Long.toString(id);
} finally {
mDatabase.endTransaction();
}
}
/**
* Deletes document and its children.
* @param documentId
*/
void deleteDocument(String documentId) {
deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, strings(documentId));
}
/**
* Gets identifier from document ID.
* @param documentId Document ID.
* @return Identifier.
* @throws FileNotFoundException
*/
Identifier createIdentifier(String documentId) throws FileNotFoundException {
// Currently documentId is old format.
final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(COLUMN_DEVICE_ID,
COLUMN_STORAGE_ID,
COLUMN_OBJECT_HANDLE,
COLUMN_DOCUMENT_TYPE),
SELECTION_DOCUMENT_ID + " AND " + COLUMN_ROW_STATE + " IN (?, ?)",
strings(documentId, ROW_STATE_VALID, ROW_STATE_INVALIDATED),
null,
null,
null,
"1");
try {
if (cursor.getCount() == 0) {
throw new FileNotFoundException("ID \"" + documentId + "\" is not found.");
} else {
cursor.moveToNext();
return new Identifier(
cursor.getInt(0),
cursor.getInt(1),
cursor.getInt(2),
documentId,
cursor.getInt(3));
}
} finally {
cursor.close();
}
}
/**
* Deletes a document, and its root information if the document is a root document.
* @param selection Query to select documents.
* @param args Arguments for selection.
* @return Whether the method deletes rows.
*/
boolean deleteDocumentsAndRootsRecursively(String selection, String[] args) {
mDatabase.beginTransaction();
try {
boolean changed = false;
final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
selection,
args,
null,
null,
null);
try {
while (cursor.moveToNext()) {
if (deleteDocumentsAndRootsRecursively(
COLUMN_PARENT_DOCUMENT_ID + " = ?",
strings(cursor.getString(0)))) {
changed = true;
}
}
} finally {
cursor.close();
}
if (deleteDocumentsAndRoots(selection, args)) {
changed = true;
}
mDatabase.setTransactionSuccessful();
return changed;
} finally {
mDatabase.endTransaction();
}
}
/**
* Marks the documents and their child as disconnected documents.
* @param selection
* @param args
* @return True if at least one row is updated.
*/
boolean disconnectDocumentsRecursively(String selection, String[] args) {
mDatabase.beginTransaction();
try {
boolean changed = false;
try (final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
selection,
args,
null,
null,
null)) {
while (cursor.moveToNext()) {
if (disconnectDocumentsRecursively(
COLUMN_PARENT_DOCUMENT_ID + " = ?",
strings(cursor.getString(0)))) {
changed = true;
}
}
}
if (disconnectDocuments(selection, args)) {
changed = true;
}
mDatabase.setTransactionSuccessful();
return changed;
} finally {
mDatabase.endTransaction();
}
}
boolean deleteDocumentsAndRoots(String selection, String[] args) {
mDatabase.beginTransaction();
try {
int deleted = 0;
deleted += mDatabase.delete(
TABLE_ROOT_EXTRA,
Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString(
false,
TABLE_DOCUMENTS,
new String[] { Document.COLUMN_DOCUMENT_ID },
selection,
null,
null,
null,
null) + ")",
args);
deleted += mDatabase.delete(TABLE_DOCUMENTS, selection, args);
mDatabase.setTransactionSuccessful();
// TODO Remove mappingState.
return deleted != 0;
} finally {
mDatabase.endTransaction();
}
}
boolean disconnectDocuments(String selection, String[] args) {
mDatabase.beginTransaction();
try {
final ContentValues values = new ContentValues();
values.put(COLUMN_ROW_STATE, ROW_STATE_DISCONNECTED);
values.putNull(COLUMN_DEVICE_ID);
values.putNull(COLUMN_STORAGE_ID);
values.putNull(COLUMN_OBJECT_HANDLE);
final boolean updated = mDatabase.update(TABLE_DOCUMENTS, values, selection, args) != 0;
mDatabase.setTransactionSuccessful();
return updated;
} finally {
mDatabase.endTransaction();
}
}
int getRowState(String documentId) throws FileNotFoundException {
try (final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(COLUMN_ROW_STATE),
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null)) {
if (cursor.getCount() == 0) {
throw new FileNotFoundException();
}
cursor.moveToNext();
return cursor.getInt(0);
}
}
void writeRowSnapshot(String documentId, ContentValues values) throws FileNotFoundException {
try (final Cursor cursor = mDatabase.query(
JOIN_ROOTS,
strings("*"),
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1")) {
if (cursor.getCount() == 0) {
throw new FileNotFoundException();
}
cursor.moveToNext();
values.clear();
DatabaseUtils.cursorRowToContentValues(cursor, values);
}
}
void updateObject(String documentId, int deviceId, String parentId, int[] operationsSupported,
MtpObjectInfo info, Long size) {
final ContentValues values = new ContentValues();
getObjectDocumentValues(values, deviceId, parentId, operationsSupported, info, size);
mDatabase.beginTransaction();
try {
mDatabase.update(
TABLE_DOCUMENTS,
values,
Document.COLUMN_DOCUMENT_ID + " = ?",
strings(documentId));
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
}
}
/**
* Obtains a document that has already mapped but has unmapped children.
* @param deviceId Device to find documents.
* @return Identifier of found document or null.
*/
@Nullable Identifier getUnmappedDocumentsParent(int deviceId) {
final String fromClosure =
TABLE_DOCUMENTS + " AS child INNER JOIN " +
TABLE_DOCUMENTS + " AS parent ON " +
"child." + COLUMN_PARENT_DOCUMENT_ID + " = " +
"parent." + Document.COLUMN_DOCUMENT_ID;
final String whereClosure =
"parent." + COLUMN_DEVICE_ID + " = ? AND " +
"parent." + COLUMN_ROW_STATE + " IN (?, ?) AND " +
"parent." + COLUMN_DOCUMENT_TYPE + " != ? AND " +
"child." + COLUMN_ROW_STATE + " = ?";
try (final Cursor cursor = mDatabase.query(
fromClosure,
strings("parent." + COLUMN_DEVICE_ID,
"parent." + COLUMN_STORAGE_ID,
"parent." + COLUMN_OBJECT_HANDLE,
"parent." + Document.COLUMN_DOCUMENT_ID,
"parent." + COLUMN_DOCUMENT_TYPE),
whereClosure,
strings(deviceId, ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_DEVICE,
ROW_STATE_DISCONNECTED),
null,
null,
null,
"1")) {
if (cursor.getCount() == 0) {
return null;
}
cursor.moveToNext();
return new Identifier(
cursor.getInt(0),
cursor.getInt(1),
cursor.getInt(2),
cursor.getString(3),
cursor.getInt(4));
}
}
/**
* Removes metadata except for data used by outgoingPersistedUriPermissions.
*/
void cleanDatabase(Uri[] outgoingPersistedUris) {
mDatabase.beginTransaction();
try {
final Set<String> ids = new HashSet<>();
for (final Uri uri : outgoingPersistedUris) {
String documentId = DocumentsContract.getDocumentId(uri);
while (documentId != null) {
if (ids.contains(documentId)) {
break;
}
ids.add(documentId);
try (final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(COLUMN_PARENT_DOCUMENT_ID),
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null)) {
documentId = cursor.moveToNext() ? cursor.getString(0) : null;
}
}
}
deleteDocumentsAndRoots(
Document.COLUMN_DOCUMENT_ID + " NOT IN " + getIdList(ids), null);
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
}
}
int getLastBootCount() {
try (final Cursor cursor = mDatabase.query(
TABLE_LAST_BOOT_COUNT, strings(COLUMN_VALUE), null, null, null, null, null)) {
if (cursor.moveToNext()) {
return cursor.getInt(0);
} else {
return 0;
}
}
}
void setLastBootCount(int value) {
Preconditions.checkArgumentNonnegative(value, "Boot count must not be negative.");
mDatabase.beginTransaction();
try {
final ContentValues values = new ContentValues();
values.put(COLUMN_VALUE, value);
mDatabase.delete(TABLE_LAST_BOOT_COUNT, null, null);
mDatabase.insert(TABLE_LAST_BOOT_COUNT, null, values);
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
}
}
private static class OpenHelper extends SQLiteOpenHelper {
public OpenHelper(Context context, int flags) {
super(context,
flags == FLAG_DATABASE_IN_MEMORY ? null : DATABASE_NAME,
null,
DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(QUERY_CREATE_DOCUMENTS);
db.execSQL(QUERY_CREATE_ROOT_EXTRA);
db.execSQL(QUERY_CREATE_LAST_BOOT_COUNT);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_DOCUMENTS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_ROOT_EXTRA);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_LAST_BOOT_COUNT);
onCreate(db);
}
}
@VisibleForTesting
static void deleteDatabase(Context context) {
context.deleteDatabase(DATABASE_NAME);
}
static void getDeviceDocumentValues(
ContentValues values,
ContentValues extraValues,
MtpDeviceRecord device) {
values.clear();
values.put(COLUMN_DEVICE_ID, device.deviceId);
values.putNull(COLUMN_STORAGE_ID);
values.putNull(COLUMN_OBJECT_HANDLE);
values.putNull(COLUMN_PARENT_DOCUMENT_ID);
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_DEVICE);
values.put(COLUMN_MAPPING_KEY, device.deviceKey);
values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
values.put(Document.COLUMN_DISPLAY_NAME, device.name);
values.putNull(Document.COLUMN_SUMMARY);
values.putNull(Document.COLUMN_LAST_MODIFIED);
values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp);
values.put(Document.COLUMN_FLAGS, getDocumentFlags(
device.operationsSupported,
Document.MIME_TYPE_DIR,
0,
MtpConstants.PROTECTION_STATUS_NONE,
// Storages are placed under device so we cannot create a document just under
// device.
DOCUMENT_TYPE_DEVICE) & ~Document.FLAG_DIR_SUPPORTS_CREATE);
values.putNull(Document.COLUMN_SIZE);
extraValues.clear();
extraValues.put(Root.COLUMN_FLAGS, getRootFlags(device.operationsSupported));
extraValues.putNull(Root.COLUMN_AVAILABLE_BYTES);
extraValues.putNull(Root.COLUMN_CAPACITY_BYTES);
extraValues.put(Root.COLUMN_MIME_TYPES, "");
}
/**
* Gets {@link ContentValues} for the given root.
* @param values {@link ContentValues} that receives values.
* @param extraValues {@link ContentValues} that receives extra values for roots.
* @param parentDocumentId Parent document ID.
* @param operationsSupported Array of Operation code supported by the device.
* @param root Root to be converted {@link ContentValues}.
*/
static void getStorageDocumentValues(
ContentValues values,
ContentValues extraValues,
String parentDocumentId,
int[] operationsSupported,
MtpRoot root) {
values.clear();
values.put(COLUMN_DEVICE_ID, root.mDeviceId);
values.put(COLUMN_STORAGE_ID, root.mStorageId);
values.putNull(COLUMN_OBJECT_HANDLE);
values.put(COLUMN_PARENT_DOCUMENT_ID, parentDocumentId);
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_STORAGE);
values.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
values.put(Document.COLUMN_DISPLAY_NAME, root.mDescription);
values.putNull(Document.COLUMN_SUMMARY);
values.putNull(Document.COLUMN_LAST_MODIFIED);
values.put(Document.COLUMN_ICON, R.drawable.ic_root_mtp);
values.put(Document.COLUMN_FLAGS, getDocumentFlags(
operationsSupported,
Document.MIME_TYPE_DIR,
0,
MtpConstants.PROTECTION_STATUS_NONE,
DOCUMENT_TYPE_STORAGE));
values.put(Document.COLUMN_SIZE, root.mMaxCapacity - root.mFreeSpace);
extraValues.put(Root.COLUMN_FLAGS, getRootFlags(operationsSupported));
extraValues.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
extraValues.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
extraValues.put(Root.COLUMN_MIME_TYPES, "");
}
/**
* Gets {@link ContentValues} for the given MTP object.
* @param values {@link ContentValues} that receives values.
* @param deviceId Device ID of the object.
* @param parentId Parent document ID of the object.
* @param info MTP object info. getCompressedSize will be ignored.
* @param size 64-bit size of documents. Negative value is regarded as unknown size.
*/
static void getObjectDocumentValues(
ContentValues values, int deviceId, String parentId,
int[] operationsSupported, MtpObjectInfo info, long size) {
values.clear();
final String mimeType = getMimeType(info);
values.put(COLUMN_DEVICE_ID, deviceId);
values.put(COLUMN_STORAGE_ID, info.getStorageId());
values.put(COLUMN_OBJECT_HANDLE, info.getObjectHandle());
values.put(COLUMN_PARENT_DOCUMENT_ID, parentId);
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
values.put(COLUMN_DOCUMENT_TYPE, DOCUMENT_TYPE_OBJECT);
values.put(Document.COLUMN_MIME_TYPE, mimeType);
values.put(Document.COLUMN_DISPLAY_NAME, info.getName());
values.putNull(Document.COLUMN_SUMMARY);
values.put(
Document.COLUMN_LAST_MODIFIED,
info.getDateModified() != 0 ? info.getDateModified() : null);
values.putNull(Document.COLUMN_ICON);
values.put(Document.COLUMN_FLAGS, getDocumentFlags(
operationsSupported, mimeType, info.getThumbCompressedSizeLong(),
info.getProtectionStatus(), DOCUMENT_TYPE_OBJECT));
if (size >= 0) {
values.put(Document.COLUMN_SIZE, size);
} else {
values.putNull(Document.COLUMN_SIZE);
}
}
private static String getMimeType(MtpObjectInfo info) {
if (info.getFormat() == MtpConstants.FORMAT_ASSOCIATION) {
return DocumentsContract.Document.MIME_TYPE_DIR;
}
return MediaFile.getMimeType(info.getName(), info.getFormat());
}
private static int getRootFlags(int[] operationsSupported) {
int rootFlag = Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_LOCAL_ONLY;
if (MtpDeviceRecord.isWritingSupported(operationsSupported)) {
rootFlag |= Root.FLAG_SUPPORTS_CREATE;
}
return rootFlag;
}
private static int getDocumentFlags(
@Nullable int[] operationsSupported, String mimeType, long thumbnailSize,
int protectionState, @DocumentType int documentType) {
int flag = 0;
if (!mimeType.equals(Document.MIME_TYPE_DIR) &&
MtpDeviceRecord.isWritingSupported(operationsSupported) &&
protectionState == MtpConstants.PROTECTION_STATUS_NONE) {
flag |= Document.FLAG_SUPPORTS_WRITE;
}
if (MtpDeviceRecord.isSupported(
operationsSupported, MtpConstants.OPERATION_DELETE_OBJECT) &&
(protectionState == MtpConstants.PROTECTION_STATUS_NONE ||
protectionState == MtpConstants.PROTECTION_STATUS_NON_TRANSFERABLE_DATA) &&
documentType == DOCUMENT_TYPE_OBJECT) {
flag |= Document.FLAG_SUPPORTS_DELETE;
}
if (mimeType.equals(Document.MIME_TYPE_DIR) &&
MtpDeviceRecord.isWritingSupported(operationsSupported) &&
protectionState == MtpConstants.PROTECTION_STATUS_NONE) {
flag |= Document.FLAG_DIR_SUPPORTS_CREATE;
}
if (MetadataReader.isSupportedMimeType(mimeType)) {
flag |= Document.FLAG_SUPPORTS_METADATA;
}
if (thumbnailSize > 0) {
flag |= Document.FLAG_SUPPORTS_THUMBNAIL;
}
return flag;
}
static String[] strings(Object... args) {
final String[] results = new String[args.length];
for (int i = 0; i < args.length; i++) {
results[i] = Objects.toString(args[i]);
}
return results;
}
static void putValuesToCursor(ContentValues values, MatrixCursor cursor) {
final RowBuilder row = cursor.newRow();
for (final String name : cursor.getColumnNames()) {
row.add(values.get(name));
}
}
private static String getIdList(Set<String> ids) {
String result = "(";
for (final String id : ids) {
if (result.length() > 1) {
result += ",";
}
result += id;
}
result += ")";
return result;
}
}