/*
 * 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 Mapper.startAddingDocuments(String) with the parent document ID. Also it needs to call 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.
/** * 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.
Returns:Mapping operations.
/** * Returns operations for mapping. * @return Mapping operations. */
Mapper getMapper() { return mMapper; }
Queries roots information.
Params:
  • columnNames – Column names defined in Root.
Returns:Database cursor.
/** * 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.
Params:
  • columnNames – Column names defined in Document.
Returns:Database cursor.
/** * 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.
Params:
  • columnNames – Column names defined in Document.
Returns:Database cursor.
/** * 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.
Params:
  • documentId – Document ID that points a device.
Throws:
Returns:Storage document IDs.
/** * 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.
Params:
  • documentId –
  • projection –
Returns:Database cursor.
/** * 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.
Params:
  • documentId –
Throws:
Returns:parent identifier.
/** * 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.
Params:
  • parentDocumentId –
  • info –
  • size – Object size. info#getCompressedSize() will be ignored because it does not contain object size more than 4GB.
Returns:Document ID of added document.
/** * 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.
Params:
  • documentId –
/** * Deletes document and its children. * @param documentId */
void deleteDocument(String documentId) { deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, strings(documentId)); }
Gets identifier from document ID.
Params:
  • documentId – Document ID.
Throws:
Returns:Identifier.
/** * 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.
Params:
  • selection – Query to select documents.
  • args – Arguments for selection.
Returns:Whether the method deletes rows.
/** * 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.
Params:
  • selection –
  • args –
Returns:True if at least one row is updated.
/** * 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.
Params:
  • deviceId – Device to find documents.
Returns:Identifier of found document or null.
/** * 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.
/** * 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 ContentValues for the given root.
Params:
  • values – ContentValues that receives values.
  • extraValues – ContentValues that receives extra values for roots.
  • parentDocumentId – Parent document ID.
  • operationsSupported – Array of Operation code supported by the device.
  • root – Root to be converted ContentValues.
/** * 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 ContentValues for the given MTP object.
Params:
  • values – ContentValues that receives values.
  • deviceId – Device ID of the object.
  • parentId – Parent document ID of the object.
  • info – MTP object info. getCompressedSize will be ignored.
  • size – 64-bit size of documents. Negative value is regarded as unknown size.
/** * 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; } final String formatCodeMimeType = MediaFile.getMimeTypeForFormatCode(info.getFormat()); final String mediaFileMimeType = MediaFile.getMimeTypeForFile(info.getName()); // Format code can be mapped with multiple mime types, e.g. FORMAT_MPEG is mapped with // audio/mp4 and video/mp4. // As file extension contains more information than format code, returns mime type obtained // from file extension if it is consistent with format code. if (mediaFileMimeType != null && MediaFile.getFormatCode("", mediaFileMimeType) == info.getFormat()) { return mediaFileMimeType; } if (formatCodeMimeType != null) { return formatCodeMimeType; } if (mediaFileMimeType != null) { return mediaFileMimeType; } // We don't know the file type. return "application/octet-stream"; } 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; } }