blob: cce619e67d198ddb6164cc652727e3bc820a0ecc [file] [log] [blame]
/*
* 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.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();
}
final RowBuilder row = result.newRow();
for (final String key : values.keySet()) {
row.add(key, values.get(key));
}
}
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,
DOCUMENT_TYPE_DEVICE));
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;
}
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;
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 (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;
}
private static String getIdList(Set<String> ids) {
String result = "(";
for (final String id : ids) {
if (result.length() > 1) {
result += ",";
}
result += id;
}
result += ")";
return result;
}
}