Extract mapping operations to partial class of MtpDatabase.

Change-Id: I5fda6d5754206528ad5953bcc179a0ffe0b4982d
This commit is contained in:
Daichi Hirono
2015-11-20 17:51:53 +09:00
parent 9e8a4fa78f
commit 259ce80132
9 changed files with 851 additions and 949 deletions

View File

@@ -255,10 +255,10 @@ class DocumentLoader {
return;
}
if (mNumLoaded == 0) {
mDatabase.startAddingChildDocuments(mIdentifier.mDocumentId);
mDatabase.getMapper().startAddingChildDocuments(mIdentifier.mDocumentId);
}
try {
mDatabase.putChildDocuments(
mDatabase.getMapper().putChildDocuments(
mIdentifier.mDeviceId, mIdentifier.mDocumentId, objectInfoList);
mNumLoaded += objectInfoList.length;
} catch (SQLiteException exp) {
@@ -266,7 +266,7 @@ class DocumentLoader {
mNumLoaded = 0;
}
if (getState() != STATE_LOADING) {
mDatabase.stopAddingChildDocuments(mIdentifier.mDocumentId);
mDatabase.getMapper().stopAddingChildDocuments(mIdentifier.mDocumentId);
}
}
@@ -275,7 +275,7 @@ class DocumentLoader {
mError = message;
mNumLoaded = 0;
if (lastState == STATE_LOADING) {
mDatabase.stopAddingChildDocuments(mIdentifier.mDocumentId);
mDatabase.getMapper().stopAddingChildDocuments(mIdentifier.mDocumentId);
}
}

View File

@@ -0,0 +1,513 @@
/*
* 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.content.ContentValues;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.mtp.MtpObjectInfo;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.HashMap;
import java.util.Map;
import static com.android.mtp.MtpDatabase.strings;
/**
* Mapping operations for MtpDatabase.
* Also see the comments of {@link MtpDatabase}.
*/
class Mapper {
private final MtpDatabase mDatabase;
/**
* Mapping mode for roots/documents where we start adding child documents.
* Methods operate the state needs to be synchronized.
*/
private final Map<String, Integer> mMappingMode = new HashMap<>();
Mapper(MtpDatabase database) {
mDatabase = database;
}
/**
* Invokes {@link #startAddingDocuments} for root documents.
* @param deviceId Device ID.
*/
synchronized void startAddingRootDocuments(int deviceId) {
final String mappingStateKey = getRootDocumentsMappingStateKey(deviceId);
Preconditions.checkState(!mMappingMode.containsKey(mappingStateKey));
mMappingMode.put(
mappingStateKey,
startAddingDocuments(
SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
}
/**
* Invokes {@link #startAddingDocuments} for child of specific documents.
* @param parentDocumentId Document ID for parent document.
*/
@VisibleForTesting
synchronized void startAddingChildDocuments(String parentDocumentId) {
final String mappingStateKey = getChildDocumentsMappingStateKey(parentDocumentId);
Preconditions.checkState(!mMappingMode.containsKey(mappingStateKey));
mMappingMode.put(
mappingStateKey,
startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
}
/**
* Puts root information to database.
* @param deviceId Device ID
* @param resources Resources required to localize root name.
* @param roots List of root information.
* @return If roots are added or removed from the database.
*/
synchronized boolean putRootDocuments(int deviceId, Resources resources, MtpRoot[] roots) {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction();
try {
final boolean heuristic;
final String mapColumn;
final String key = getRootDocumentsMappingStateKey(deviceId);
Preconditions.checkState(mMappingMode.containsKey(key));
switch (mMappingMode.get(key)) {
case MAP_BY_MTP_IDENTIFIER:
heuristic = false;
mapColumn = COLUMN_STORAGE_ID;
break;
case MAP_BY_NAME:
heuristic = true;
mapColumn = Document.COLUMN_DISPLAY_NAME;
break;
default:
throw new Error("Unexpected map mode.");
}
final ContentValues[] valuesList = new ContentValues[roots.length];
for (int i = 0; i < roots.length; i++) {
if (roots[i].mDeviceId != deviceId) {
throw new IllegalArgumentException();
}
valuesList[i] = new ContentValues();
MtpDatabase.getRootDocumentValues(valuesList[i], resources, roots[i]);
}
final boolean changed = putDocuments(
valuesList,
SELECTION_ROOT_DOCUMENTS,
Integer.toString(deviceId),
heuristic,
mapColumn);
final ContentValues values = new ContentValues();
int i = 0;
for (final MtpRoot root : roots) {
// Use the same value for the root ID and the corresponding document ID.
final String documentId = valuesList[i++].getAsString(Document.COLUMN_DOCUMENT_ID);
// If it fails to insert/update documents, the document ID will be set with -1.
// In this case we don't insert/update root extra information neither.
if (documentId == null) {
continue;
}
values.put(Root.COLUMN_ROOT_ID, documentId);
values.put(
Root.COLUMN_FLAGS,
Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
values.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
values.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
values.put(Root.COLUMN_MIME_TYPES, "");
database.replace(TABLE_ROOT_EXTRA, null, values);
}
database.setTransactionSuccessful();
return changed;
} finally {
database.endTransaction();
}
}
/**
* Puts document information to database.
* @param deviceId Device ID
* @param parentId Parent document ID.
* @param documents List of document information.
*/
synchronized void putChildDocuments(int deviceId, String parentId, MtpObjectInfo[] documents) {
final boolean heuristic;
final String mapColumn;
final String key = getChildDocumentsMappingStateKey(parentId);
Preconditions.checkState(mMappingMode.containsKey(key));
switch (mMappingMode.get(key)) {
case MAP_BY_MTP_IDENTIFIER:
heuristic = false;
mapColumn = COLUMN_OBJECT_HANDLE;
break;
case MAP_BY_NAME:
heuristic = true;
mapColumn = Document.COLUMN_DISPLAY_NAME;
break;
default:
throw new Error("Unexpected map mode.");
}
final ContentValues[] valuesList = new ContentValues[documents.length];
for (int i = 0; i < documents.length; i++) {
valuesList[i] = new ContentValues();
MtpDatabase.getChildDocumentValues(
valuesList[i], deviceId, parentId, documents[i]);
}
putDocuments(
valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
}
/**
* Stops adding root documents.
* @param deviceId Device ID.
* @return True if new rows are added/removed.
*/
synchronized boolean stopAddingRootDocuments(int deviceId) {
final String key = getRootDocumentsMappingStateKey(deviceId);
Preconditions.checkState(mMappingMode.containsKey(key));
switch (mMappingMode.get(key)) {
case MAP_BY_MTP_IDENTIFIER:
mMappingMode.remove(key);
return stopAddingDocuments(
SELECTION_ROOT_DOCUMENTS,
Integer.toString(deviceId),
COLUMN_STORAGE_ID);
case MAP_BY_NAME:
mMappingMode.remove(key);
return stopAddingDocuments(
SELECTION_ROOT_DOCUMENTS,
Integer.toString(deviceId),
Document.COLUMN_DISPLAY_NAME);
default:
throw new Error("Unexpected mapping state.");
}
}
/**
* Stops adding documents under the parent.
* @param parentId Document ID of the parent.
*/
synchronized void stopAddingChildDocuments(String parentId) {
final String key = getChildDocumentsMappingStateKey(parentId);
Preconditions.checkState(mMappingMode.containsKey(key));
switch (mMappingMode.get(key)) {
case MAP_BY_MTP_IDENTIFIER:
stopAddingDocuments(
SELECTION_CHILD_DOCUMENTS,
parentId,
COLUMN_OBJECT_HANDLE);
break;
case MAP_BY_NAME:
stopAddingDocuments(
SELECTION_CHILD_DOCUMENTS,
parentId,
Document.COLUMN_DISPLAY_NAME);
break;
default:
throw new Error("Unexpected mapping state.");
}
mMappingMode.remove(key);
}
@VisibleForTesting
void clearMapping() {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction();
try {
mDatabase.deleteDocumentsAndRootsRecursively(
COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
final ContentValues values = new ContentValues();
values.putNull(COLUMN_OBJECT_HANDLE);
values.putNull(COLUMN_STORAGE_ID);
values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
database.update(TABLE_DOCUMENTS, values, null, null);
database.setTransactionSuccessful();
mMappingMode.clear();
} finally {
database.endTransaction();
}
}
/**
* Starts adding new documents.
* The methods decides mapping mode depends on if all documents under the given parent have MTP
* identifier or not. If all the documents have MTP identifier, it uses the identifier to find
* a corresponding existing row. Otherwise it does heuristic.
*
* @param selection Query matches valid documents.
* @param arg Argument for selection.
* @return Mapping mode.
*/
private int startAddingDocuments(String selection, String arg) {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction();
try {
// Delete all pending rows.
mDatabase.deleteDocumentsAndRootsRecursively(
selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));
// Set all documents as invalidated.
final ContentValues values = new ContentValues();
values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
database.update(TABLE_DOCUMENTS, values, selection, new String[] { arg });
// If we have rows that does not have MTP identifier, do heuristic mapping by name.
final boolean useNameForResolving = DatabaseUtils.queryNumEntries(
database,
TABLE_DOCUMENTS,
selection + " AND " + COLUMN_STORAGE_ID + " IS NULL",
new String[] { arg }) > 0;
database.setTransactionSuccessful();
return useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER;
} finally {
database.endTransaction();
}
}
/**
* Puts the documents into the database.
* If the mapping mode is not heuristic, it just adds the rows to the database or updates the
* existing rows with the new values. If the mapping mode is heuristic, it adds some new rows as
* 'pending' state when that rows may be corresponding to existing 'invalidated' rows. Then
* {@link #stopAddingDocuments(String, String, String)} turns the pending rows into 'valid'
* rows. If the methods adds rows to database, it updates valueList with correct document ID.
*
* @param valuesList Values for documents to be stored in the database.
* @param selection SQL where closure to select rows that shares the same parent.
* @param arg Argument for selection SQL.
* @param heuristic Whether the mapping mode is heuristic.
* @return Whether the method adds new rows.
*/
private boolean putDocuments(
ContentValues[] valuesList,
String selection,
String arg,
boolean heuristic,
String mappingKey) {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
boolean added = false;
database.beginTransaction();
try {
for (final ContentValues values : valuesList) {
final Cursor candidateCursor = database.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
selection + " AND " +
COLUMN_ROW_STATE + "=? AND " +
mappingKey + "=?",
strings(arg, ROW_STATE_INVALIDATED, values.getAsString(mappingKey)),
null,
null,
null,
"1");
try {
final long rowId;
if (candidateCursor.getCount() == 0) {
rowId = database.insert(TABLE_DOCUMENTS, null, values);
if (rowId == -1) {
throw new SQLiteException("Failed to put a document into database.");
}
added = true;
} else if (!heuristic) {
candidateCursor.moveToNext();
final String documentId = candidateCursor.getString(0);
rowId = database.update(
TABLE_DOCUMENTS,
values,
SELECTION_DOCUMENT_ID,
strings(documentId));
} else {
values.put(COLUMN_ROW_STATE, ROW_STATE_PENDING);
rowId = database.insert(TABLE_DOCUMENTS, null, values);
}
// Document ID is a primary integer key of the table. So the returned row
// IDs should be same with the document ID.
values.put(Document.COLUMN_DOCUMENT_ID, rowId);
} finally {
candidateCursor.close();
}
}
database.setTransactionSuccessful();
return added;
} finally {
database.endTransaction();
}
}
/**
* Maps 'pending' document and 'invalidated' document that shares the same column of groupKey.
* If the database does not find corresponding 'invalidated' document, it just removes
* 'invalidated' document from the database.
* @param selection Query to select rows for resolving.
* @param arg Argument for selection SQL.
* @param groupKey Column name used to find corresponding rows.
* @return Whether the methods adds or removed visible rows.
*/
private boolean stopAddingDocuments(String selection, String arg, String groupKey) {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction();
try {
// Get 1-to-1 mapping of invalidated document and pending document.
final String invalidatedIdQuery = createStateFilter(
ROW_STATE_INVALIDATED, Document.COLUMN_DOCUMENT_ID);
final String pendingIdQuery = createStateFilter(
ROW_STATE_PENDING, Document.COLUMN_DOCUMENT_ID);
// SQL should be like:
// SELECT group_concat(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END),
// group_concat(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END)
// WHERE device_id = ? AND parent_document_id IS NULL
// GROUP BY display_name
// HAVING count(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END) = 1 AND
// count(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END) = 1
final Cursor mergingCursor = database.query(
TABLE_DOCUMENTS,
new String[] {
"group_concat(" + invalidatedIdQuery + ")",
"group_concat(" + pendingIdQuery + ")"
},
selection,
strings(arg),
groupKey,
"count(" + invalidatedIdQuery + ") = 1 AND count(" + pendingIdQuery + ") = 1",
null);
final ContentValues values = new ContentValues();
while (mergingCursor.moveToNext()) {
final String invalidatedId = mergingCursor.getString(0);
final String pendingId = mergingCursor.getString(1);
// Obtain the new values including the latest object handle from mapping row.
getFirstRow(
TABLE_DOCUMENTS,
SELECTION_DOCUMENT_ID,
new String[] { pendingId },
values);
values.remove(Document.COLUMN_DOCUMENT_ID);
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
database.update(
TABLE_DOCUMENTS,
values,
SELECTION_DOCUMENT_ID,
new String[] { invalidatedId });
getFirstRow(
TABLE_ROOT_EXTRA,
SELECTION_ROOT_ID,
new String[] { pendingId },
values);
if (values.size() > 0) {
values.remove(Root.COLUMN_ROOT_ID);
database.update(
TABLE_ROOT_EXTRA,
values,
SELECTION_ROOT_ID,
new String[] { invalidatedId });
}
// Delete 'pending' row.
mDatabase.deleteDocumentsAndRootsRecursively(
SELECTION_DOCUMENT_ID, new String[] { pendingId });
}
mergingCursor.close();
boolean changed = false;
// Delete all invalidated rows that cannot be mapped.
if (mDatabase.deleteDocumentsAndRootsRecursively(
COLUMN_ROW_STATE + " = ? AND " + selection,
strings(ROW_STATE_INVALIDATED, arg))) {
changed = true;
}
// The database cannot find old document ID for the pending rows.
// Turn the all pending rows into valid state, which means the rows become to be
// valid with new document ID.
values.clear();
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
if (database.update(
TABLE_DOCUMENTS,
values,
COLUMN_ROW_STATE + " = ? AND " + selection,
strings(ROW_STATE_PENDING, arg)) != 0) {
changed = true;
}
database.setTransactionSuccessful();
return changed;
} finally {
database.endTransaction();
}
}
/**
* Obtains values of the first row for the query.
* @param values ContentValues that the values are stored to.
* @param table Target table.
* @param selection Query to select rows.
* @param args Argument for query.
*/
private void getFirstRow(String table, String selection, String[] args, ContentValues values) {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
values.clear();
final Cursor cursor = database.query(table, null, selection, args, null, null, null, "1");
if (cursor.getCount() == 0) {
return;
}
cursor.moveToNext();
DatabaseUtils.cursorRowToContentValues(cursor, values);
cursor.close();
}
/**
* Gets SQL expression that represents the given value or NULL depends on the row state.
* You must pass static constants to this methods otherwise you may be suffered from SQL
* injections.
* @param state Expected row state.
* @param a SQL value.
* @return Expression that represents a if the row state is expected one, and represents NULL
* otherwise.
*/
private static String createStateFilter(int state, String a) {
return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
" THEN " + a + " ELSE NULL END";
}
/**
* @param deviceId Device ID.
* @return Key for {@link #mMappingMode}.
*/
private static String getRootDocumentsMappingStateKey(int deviceId) {
return "RootDocuments/" + deviceId;
}
/**
* @param parentDocumentId Document ID for the parent document.
* @return Key for {@link #mMappingMode}.
*/
private static String getChildDocumentsMappingStateKey(String parentDocumentId) {
return "ChildDocuments/" + parentDocumentId;
}
}

View File

@@ -22,6 +22,9 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
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;
@@ -32,8 +35,7 @@ import android.provider.DocumentsContract.Root;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Database for MTP objects.
@@ -49,13 +51,13 @@ import java.util.Map;
* by comparing the directory structure and object name.
*
* To start putting documents into the database, the client needs to call
* {@link #startAddingChildDocuments(String)} with the parent document ID. Also it needs to call
* {@link #stopAddingChildDocuments(String)} after putting all child documents to the database.
* (All explanations are same for root documents)
* {@link Mapper#startAddingChildDocuments(String)} with the parent document ID. Also it
* needs to call {@link Mapper#stopAddingChildDocuments(String)} after putting all child
* documents to the database. (All explanations are same for root documents)
*
* database.startAddingChildDocuments();
* database.putChildDocuments();
* database.stopAddingChildDocuments();
* database.getMapper().startAddingChildDocuments();
* database.getMapper().putChildDocuments();
* database.getMapper().stopAddingChildDocuments();
*
* 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
@@ -72,186 +74,229 @@ import java.util.Map;
* TODO: Improve performance by SQL optimization.
*/
class MtpDatabase {
private final MtpDatabaseInternal mDatabase;
private final SQLiteDatabase mDatabase;
private final Mapper mMapper;
/**
* Mapping mode for roots/documents where we start adding child documents.
* Methods operate the state needs to be synchronized.
*/
private final Map<String, Integer> mMappingMode = new HashMap<>();
@VisibleForTesting
MtpDatabase(Context context, int flags) {
mDatabase = new MtpDatabaseInternal(context, flags);
SQLiteDatabase getSQLiteDatabase() {
return mDatabase;
}
MtpDatabase(Context context, int flags) {
final OpenHelper helper = new OpenHelper(context, flags);
mDatabase = helper.getWritableDatabase();
mMapper = new Mapper(this);
}
/**
* Closes the database.
*/
@VisibleForTesting
void close() {
mDatabase.close();
}
/**
* {@link MtpDatabaseInternal#queryRoots}
* Returns operations for mapping.
* @return Mapping operations.
*/
Cursor queryRoots(String[] columnNames) {
return mDatabase.queryRoots(columnNames);
Mapper getMapper() {
return mMapper;
}
/**
* {@link MtpDatabaseInternal#queryRootDocuments}
* Queries roots information.
* @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}.
* @return Database cursor.
*/
Cursor queryRoots(String[] columnNames) {
return mDatabase.query(
VIEW_ROOTS,
columnNames,
COLUMN_ROW_STATE + " IN (?, ?)",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
null,
null,
null);
}
/**
* 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.queryRootDocuments(columnNames);
return mDatabase.query(
TABLE_DOCUMENTS,
columnNames,
COLUMN_ROW_STATE + " IN (?, ?)",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
null,
null,
null);
}
/**
* {@link MtpDatabaseInternal#queryChildDocuments}
* 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.queryChildDocuments(columnNames, 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);
}
/**
* {@link MtpDatabaseInternal#queryDocument}
* Queries a single document.
* @param documentId
* @param projection
* @return Database cursor.
*/
Cursor queryDocument(String documentId, String[] projection) {
return mDatabase.queryDocument(documentId, projection);
public Cursor queryDocument(String documentId, String[] projection) {
return mDatabase.query(
TABLE_DOCUMENTS,
projection,
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1");
}
/**
* {@link MtpDatabaseInternal#createIdentifier}
*/
Identifier createIdentifier(String parentDocumentId) throws FileNotFoundException {
return mDatabase.createIdentifier(parentDocumentId);
}
/**
* {@link MtpDatabaseInternal#removeDeviceRows}
* Remove all rows belong to a device.
* @param deviceId Device ID.
*/
void removeDeviceRows(int deviceId) {
mDatabase.removeDeviceRows(deviceId);
// Call non-recursive version because it anyway deletes all rows in the devices.
deleteDocumentsAndRoots(COLUMN_DEVICE_ID + "=?", strings(deviceId));
}
/**
* {@link MtpDatabaseInternal#getParentId}
* Obtains parent document ID.
* @param documentId
* @return parent document ID.
* @throws FileNotFoundException
*/
String getParentId(String documentId) throws FileNotFoundException {
return mDatabase.getParentId(documentId);
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 cursor.getString(0);
} else {
throw new FileNotFoundException("Cannot find a row having ID=" + documentId);
}
} finally {
cursor.close();
}
}
/**
* {@link MtpDatabaseInternal#deleteDocument}
* 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
* @return Document ID of added document.
*/
void deleteDocument(String documentId) {
mDatabase.deleteDocument(documentId);
}
/**
* {@link MtpDatabaseInternal#putNewDocument}
* @throws FileNotFoundException
*/
String putNewDocument(int deviceId, String parentDocumentId, MtpObjectInfo info)
throws FileNotFoundException {
String putNewDocument(int deviceId, String parentDocumentId, MtpObjectInfo info) {
final ContentValues values = new ContentValues();
getChildDocumentValues(values, deviceId, parentDocumentId, info);
return mDatabase.putNewDocument(parentDocumentId, values);
}
/**
* Invokes {@link MtpDatabaseInternal#startAddingDocuments} for root documents.
* @param deviceId Device ID.
*/
synchronized void startAddingRootDocuments(int deviceId) {
final String mappingStateKey = getRootDocumentsMappingStateKey(deviceId);
if (mMappingMode.containsKey(mappingStateKey)) {
throw new Error("Mapping for the root has already started.");
}
mMappingMode.put(
mappingStateKey,
mDatabase.startAddingDocuments(
SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
}
/**
* Invokes {@link MtpDatabaseInternal#startAddingDocuments} for child of specific documents.
* @param parentDocumentId Document ID for parent document.
*/
@VisibleForTesting
synchronized void startAddingChildDocuments(String parentDocumentId) {
final String mappingStateKey = getChildDocumentsMappingStateKey(parentDocumentId);
if (mMappingMode.containsKey(mappingStateKey)) {
throw new Error("Mapping for the root has already started.");
}
mMappingMode.put(
mappingStateKey,
mDatabase.startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
}
/**
* Puts root information to database.
* @param deviceId Device ID
* @param resources Resources required to localize root name.
* @param roots List of root information.
* @return If roots are added or removed from the database.
*/
synchronized boolean putRootDocuments(int deviceId, Resources resources, MtpRoot[] roots) {
mDatabase.beginTransaction();
try {
final boolean heuristic;
final String mapColumn;
final String key = getRootDocumentsMappingStateKey(deviceId);
if (!mMappingMode.containsKey(key)) {
throw new IllegalStateException("startAddingRootDocuments has not been called.");
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),
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1");
try {
if (cursor.getCount() == 0) {
throw new FileNotFoundException("ID is not found.");
} else {
cursor.moveToNext();
return new Identifier(
cursor.getInt(0),
cursor.getInt(1),
cursor.isNull(2) ? Identifier.DUMMY_HANDLE_FOR_ROOT : cursor.getInt(2),
documentId);
}
switch (mMappingMode.get(key)) {
case MAP_BY_MTP_IDENTIFIER:
heuristic = false;
mapColumn = COLUMN_STORAGE_ID;
break;
case MAP_BY_NAME:
heuristic = true;
mapColumn = Document.COLUMN_DISPLAY_NAME;
break;
default:
throw new Error("Unexpected map mode.");
}
final ContentValues[] valuesList = new ContentValues[roots.length];
for (int i = 0; i < roots.length; i++) {
if (roots[i].mDeviceId != deviceId) {
throw new IllegalArgumentException();
} 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;
}
}
valuesList[i] = new ContentValues();
getRootDocumentValues(valuesList[i], resources, roots[i]);
} finally {
cursor.close();
}
final boolean changed = mDatabase.putDocuments(
valuesList,
SELECTION_ROOT_DOCUMENTS,
Integer.toString(deviceId),
heuristic,
mapColumn);
final ContentValues values = new ContentValues();
int i = 0;
for (final MtpRoot root : roots) {
// Use the same value for the root ID and the corresponding document ID.
final String documentId = valuesList[i++].getAsString(Document.COLUMN_DOCUMENT_ID);
// If it fails to insert/update documents, the document ID will be set with -1.
// In this case we don't insert/update root extra information neither.
if (documentId == null) {
continue;
}
values.put(Root.COLUMN_ROOT_ID, documentId);
values.put(
Root.COLUMN_FLAGS,
Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
values.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
values.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
values.put(Root.COLUMN_MIME_TYPES, "");
mDatabase.putRootExtra(values);
if (deleteDocumentsAndRoots(selection, args)) {
changed = true;
}
mDatabase.setTransactionSuccessful();
return changed;
@@ -260,95 +305,50 @@ class MtpDatabase {
}
}
/**
* Puts document information to database.
* @param deviceId Device ID
* @param parentId Parent document ID.
* @param documents List of document information.
*/
@VisibleForTesting
synchronized void putChildDocuments(int deviceId, String parentId, MtpObjectInfo[] documents) {
final boolean heuristic;
final String mapColumn;
switch (mMappingMode.get(getChildDocumentsMappingStateKey(parentId))) {
case MAP_BY_MTP_IDENTIFIER:
heuristic = false;
mapColumn = COLUMN_OBJECT_HANDLE;
break;
case MAP_BY_NAME:
heuristic = true;
mapColumn = Document.COLUMN_DISPLAY_NAME;
break;
default:
throw new Error("Unexpected map mode.");
}
final ContentValues[] valuesList = new ContentValues[documents.length];
for (int i = 0; i < documents.length; i++) {
valuesList[i] = new ContentValues();
getChildDocumentValues(valuesList[i], deviceId, parentId, documents[i]);
}
mDatabase.putDocuments(
valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
}
/**
* Clears mapping between MTP identifier and document/root ID.
*/
@VisibleForTesting
synchronized void clearMapping() {
mDatabase.clearMapping();
mMappingMode.clear();
}
/**
* Stops adding root documents.
* @param deviceId Device ID.
* @return True if new rows are added/removed.
*/
synchronized boolean stopAddingRootDocuments(int deviceId) {
final String mappingModeKey = getRootDocumentsMappingStateKey(deviceId);
switch (mMappingMode.get(mappingModeKey)) {
case MAP_BY_MTP_IDENTIFIER:
mMappingMode.remove(mappingModeKey);
return mDatabase.stopAddingDocuments(
SELECTION_ROOT_DOCUMENTS,
Integer.toString(deviceId),
COLUMN_STORAGE_ID);
case MAP_BY_NAME:
mMappingMode.remove(mappingModeKey);
return mDatabase.stopAddingDocuments(
SELECTION_ROOT_DOCUMENTS,
Integer.toString(deviceId),
Document.COLUMN_DISPLAY_NAME);
default:
throw new Error("Unexpected mapping state.");
private 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();
}
}
/**
* Stops adding documents under the parent.
* @param parentId Document ID of the parent.
*/
@VisibleForTesting
synchronized void stopAddingChildDocuments(String parentId) {
final String mappingModeKey = getChildDocumentsMappingStateKey(parentId);
switch (mMappingMode.get(mappingModeKey)) {
case MAP_BY_MTP_IDENTIFIER:
mDatabase.stopAddingDocuments(
SELECTION_CHILD_DOCUMENTS,
parentId,
COLUMN_OBJECT_HANDLE);
break;
case MAP_BY_NAME:
mDatabase.stopAddingDocuments(
SELECTION_CHILD_DOCUMENTS,
parentId,
Document.COLUMN_DISPLAY_NAME);
break;
default:
throw new Error("Unexpected mapping state.");
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_VIEW_ROOTS);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new UnsupportedOperationException();
}
mMappingMode.remove(mappingModeKey);
}
/**
@@ -357,8 +357,7 @@ class MtpDatabase {
* @param resources Resources used to get localized root name.
* @param root Root to be converted {@link ContentValues}.
*/
private static void getRootDocumentValues(
ContentValues values, Resources resources, MtpRoot root) {
static void getRootDocumentValues(ContentValues values, Resources resources, MtpRoot root) {
values.clear();
values.put(COLUMN_DEVICE_ID, root.mDeviceId);
values.put(COLUMN_STORAGE_ID, root.mStorageId);
@@ -382,7 +381,7 @@ class MtpDatabase {
* @param parentId Parent document ID of the object.
* @param info MTP object info.
*/
private void getChildDocumentValues(
static void getChildDocumentValues(
ContentValues values, int deviceId, String parentId, MtpObjectInfo info) {
values.clear();
final String mimeType = info.getFormat() == MtpConstants.FORMAT_ASSOCIATION ?
@@ -415,19 +414,11 @@ class MtpDatabase {
values.put(Document.COLUMN_SIZE, info.getCompressedSize());
}
/**
* @param deviceId Device ID.
* @return Key for {@link #mMappingMode}.
*/
private static String getRootDocumentsMappingStateKey(int deviceId) {
return "RootDocuments/" + deviceId;
}
/**
* @param parentDocumentId Document ID for the parent document.
* @return Key for {@link #mMappingMode}.
*/
private static String getChildDocumentsMappingStateKey(String parentDocumentId) {
return "ChildDocuments/" + parentDocumentId;
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;
}
}

View File

@@ -1,599 +0,0 @@
/*
* 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.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import java.io.FileNotFoundException;
import java.util.Objects;
/**
* Class that provides operations processing SQLite database directly.
*/
class MtpDatabaseInternal {
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_VIEW_ROOTS);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
throw new UnsupportedOperationException();
}
}
private final SQLiteDatabase mDatabase;
MtpDatabaseInternal(Context context, int flags) {
final OpenHelper helper = new OpenHelper(context, flags);
mDatabase = helper.getWritableDatabase();
}
void close() {
mDatabase.close();
}
/**
* Queries roots information.
* @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}.
* @return Database cursor.
*/
Cursor queryRoots(String[] columnNames) {
return mDatabase.query(
VIEW_ROOTS,
columnNames,
COLUMN_ROW_STATE + " IN (?, ?)",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
null,
null,
null);
}
/**
* Queries root documents information.
* @param columnNames Column names defined in
* {@link android.provider.DocumentsContract.Document}.
* @return Database cursor.
*/
Cursor queryRootDocuments(String[] columnNames) {
return mDatabase.query(
TABLE_DOCUMENTS,
columnNames,
COLUMN_ROW_STATE + " IN (?, ?)",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
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);
}
/**
* Queries a single document.
* @param documentId
* @param projection
* @return Database cursor.
*/
public Cursor queryDocument(String documentId, String[] projection) {
return mDatabase.query(
TABLE_DOCUMENTS,
projection,
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1");
}
/**
* Remove all rows belong to a device.
* @param deviceId Device ID.
*/
void removeDeviceRows(int deviceId) {
// Call non-recursive version because it anyway deletes all rows in the devices.
deleteDocumentsAndRoots(COLUMN_DEVICE_ID + "=?", strings(deviceId));
}
/**
* Obtains parent document ID.
* @param documentId
* @return parent document ID.
* @throws FileNotFoundException
*/
String getParentId(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 cursor.getString(0);
} else {
throw new FileNotFoundException("Cannot find a row having ID=" + documentId);
}
} finally {
cursor.close();
}
}
/**
* Deletes document and its children.
* @param documentId
*/
void deleteDocument(String documentId) {
deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, strings(documentId));
}
/**
* 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 values
* @return Document ID of added document.
*/
String putNewDocument(String parentDocumentId, ContentValues values) {
mDatabase.beginTransaction();
try {
final long id = mDatabase.insert(TABLE_DOCUMENTS, null, values);
mDatabase.setTransactionSuccessful();
return Long.toString(id);
} finally {
mDatabase.endTransaction();
}
}
/**
* Gets identifier from document ID.
* @param documentId Document ID.
* @return Identifier.
* @throws FileNotFoundException
*/
Identifier createIdentifier(String documentId) throws FileNotFoundException {
final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(COLUMN_DEVICE_ID, COLUMN_STORAGE_ID, COLUMN_OBJECT_HANDLE),
SELECTION_DOCUMENT_ID,
strings(documentId),
null,
null,
null,
"1");
try {
if (cursor.getCount() == 0) {
throw new FileNotFoundException("ID is not found.");
} else {
cursor.moveToNext();
return new Identifier(
cursor.getInt(0),
cursor.getInt(1),
cursor.isNull(2) ? Identifier.DUMMY_HANDLE_FOR_ROOT : cursor.getInt(2),
documentId);
}
} finally {
cursor.close();
}
}
/**
* Starts adding new documents.
* The methods decides mapping mode depends on if all documents under the given parent have MTP
* identifier or not. If all the documents have MTP identifier, it uses the identifier to find
* a corresponding existing row. Otherwise it does heuristic.
*
* @param selection Query matches valid documents.
* @param arg Argument for selection.
* @return Mapping mode.
*/
int startAddingDocuments(String selection, String arg) {
mDatabase.beginTransaction();
try {
// Delete all pending rows.
deleteDocumentsAndRootsRecursively(
selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));
// Set all documents as invalidated.
final ContentValues values = new ContentValues();
values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
mDatabase.update(TABLE_DOCUMENTS, values, selection, new String[] { arg });
// If we have rows that does not have MTP identifier, do heuristic mapping by name.
final boolean useNameForResolving = DatabaseUtils.queryNumEntries(
mDatabase,
TABLE_DOCUMENTS,
selection + " AND " + COLUMN_STORAGE_ID + " IS NULL",
new String[] { arg }) > 0;
mDatabase.setTransactionSuccessful();
return useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER;
} finally {
mDatabase.endTransaction();
}
}
/**
* Puts the documents into the database.
* If the mapping mode is not heuristic, it just adds the rows to the database or updates the
* existing rows with the new values. If the mapping mode is heuristic, it adds some new rows as
* 'pending' state when that rows may be corresponding to existing 'invalidated' rows. Then
* {@link #stopAddingDocuments(String, String, String)} turns the pending rows into 'valid'
* rows. If the methods adds rows to database, it updates valueList with correct document ID.
*
* @param valuesList Values for documents to be stored in the database.
* @param selection SQL where closure to select rows that shares the same parent.
* @param arg Argument for selection SQL.
* @param heuristic Whether the mapping mode is heuristic.
* @return Whether the method adds new rows.
*/
boolean putDocuments(
ContentValues[] valuesList,
String selection,
String arg,
boolean heuristic,
String mappingKey) {
boolean added = false;
mDatabase.beginTransaction();
try {
for (final ContentValues values : valuesList) {
final Cursor candidateCursor = mDatabase.query(
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
selection + " AND " +
COLUMN_ROW_STATE + "=? AND " +
mappingKey + "=?",
strings(arg, ROW_STATE_INVALIDATED, values.getAsString(mappingKey)),
null,
null,
null,
"1");
try {
final long rowId;
if (candidateCursor.getCount() == 0) {
rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
if (rowId == -1) {
throw new SQLiteException("Failed to put a document into database.");
}
added = true;
} else if (!heuristic) {
candidateCursor.moveToNext();
final String documentId = candidateCursor.getString(0);
rowId = mDatabase.update(
TABLE_DOCUMENTS,
values,
SELECTION_DOCUMENT_ID,
strings(documentId));
} else {
values.put(COLUMN_ROW_STATE, ROW_STATE_PENDING);
rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
}
// Document ID is a primary integer key of the table. So the returned row
// IDs should be same with the document ID.
values.put(Document.COLUMN_DOCUMENT_ID, rowId);
} finally {
candidateCursor.close();
}
}
mDatabase.setTransactionSuccessful();
return added;
} finally {
mDatabase.endTransaction();
}
}
/**
* Puts extra information for root documents.
* @param values Values containing extra information.
*/
void putRootExtra(ContentValues values) {
mDatabase.replace(TABLE_ROOT_EXTRA, null, values);
}
/**
* Maps 'pending' document and 'invalidated' document that shares the same column of groupKey.
* If the database does not find corresponding 'invalidated' document, it just removes
* 'invalidated' document from the database.
* @param selection Query to select rows for resolving.
* @param arg Argument for selection SQL.
* @param groupKey Column name used to find corresponding rows.
* @return Whether the methods adds or removed visible rows.
*/
boolean stopAddingDocuments(String selection, String arg, String groupKey) {
mDatabase.beginTransaction();
try {
// Get 1-to-1 mapping of invalidated document and pending document.
final String invalidatedIdQuery = createStateFilter(
ROW_STATE_INVALIDATED, Document.COLUMN_DOCUMENT_ID);
final String pendingIdQuery = createStateFilter(
ROW_STATE_PENDING, Document.COLUMN_DOCUMENT_ID);
// SQL should be like:
// SELECT group_concat(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END),
// group_concat(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END)
// WHERE device_id = ? AND parent_document_id IS NULL
// GROUP BY display_name
// HAVING count(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END) = 1 AND
// count(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END) = 1
final Cursor mergingCursor = mDatabase.query(
TABLE_DOCUMENTS,
new String[] {
"group_concat(" + invalidatedIdQuery + ")",
"group_concat(" + pendingIdQuery + ")"
},
selection,
strings(arg),
groupKey,
"count(" + invalidatedIdQuery + ") = 1 AND count(" + pendingIdQuery + ") = 1",
null);
final ContentValues values = new ContentValues();
while (mergingCursor.moveToNext()) {
final String invalidatedId = mergingCursor.getString(0);
final String pendingId = mergingCursor.getString(1);
// Obtain the new values including the latest object handle from mapping row.
getFirstRow(
TABLE_DOCUMENTS,
SELECTION_DOCUMENT_ID,
new String[] { pendingId },
values);
values.remove(Document.COLUMN_DOCUMENT_ID);
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
mDatabase.update(
TABLE_DOCUMENTS,
values,
SELECTION_DOCUMENT_ID,
new String[] { invalidatedId });
getFirstRow(
TABLE_ROOT_EXTRA,
SELECTION_ROOT_ID,
new String[] { pendingId },
values);
if (values.size() > 0) {
values.remove(Root.COLUMN_ROOT_ID);
mDatabase.update(
TABLE_ROOT_EXTRA,
values,
SELECTION_ROOT_ID,
new String[] { invalidatedId });
}
// Delete 'pending' row.
deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, new String[] { pendingId });
}
mergingCursor.close();
boolean changed = false;
// Delete all invalidated rows that cannot be mapped.
if (deleteDocumentsAndRootsRecursively(
COLUMN_ROW_STATE + " = ? AND " + selection,
strings(ROW_STATE_INVALIDATED, arg))) {
changed = true;
}
// The database cannot find old document ID for the pending rows.
// Turn the all pending rows into valid state, which means the rows become to be
// valid with new document ID.
values.clear();
values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
if (mDatabase.update(
TABLE_DOCUMENTS,
values,
COLUMN_ROW_STATE + " = ? AND " + selection,
strings(ROW_STATE_PENDING, arg)) != 0) {
changed = true;
}
mDatabase.setTransactionSuccessful();
return changed;
} finally {
mDatabase.endTransaction();
}
}
/**
* Clears MTP related identifier.
* It clears MTP's object handle and storage ID that are not stable over MTP sessions and mark
* the all documents as 'invalidated'. It also remove 'pending' rows as adding is cancelled
* now.
*/
void clearMapping() {
mDatabase.beginTransaction();
try {
deleteDocumentsAndRootsRecursively(
COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
final ContentValues values = new ContentValues();
values.putNull(COLUMN_OBJECT_HANDLE);
values.putNull(COLUMN_STORAGE_ID);
values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
mDatabase.update(TABLE_DOCUMENTS, values, null, null);
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
}
}
/**
* {@link android.database.sqlite.SQLiteDatabase#beginTransaction()}
*/
void beginTransaction() {
mDatabase.beginTransaction();
}
/**
* {@link android.database.sqlite.SQLiteDatabase#setTransactionSuccessful()}
*/
void setTransactionSuccessful() {
mDatabase.setTransactionSuccessful();
}
/**
* {@link android.database.sqlite.SQLiteDatabase#endTransaction()}
*/
void endTransaction() {
mDatabase.endTransaction();
}
/**
* 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.
*/
private 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();
}
}
private 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();
}
}
/**
* Obtains values of the first row for the query.
* @param values ContentValues that the values are stored to.
* @param table Target table.
* @param selection Query to select rows.
* @param args Argument for query.
*/
private void getFirstRow(String table, String selection, String[] args, ContentValues values) {
values.clear();
final Cursor cursor = mDatabase.query(table, null, selection, args, null, null, null, "1");
if (cursor.getCount() == 0) {
return;
}
cursor.moveToNext();
DatabaseUtils.cursorRowToContentValues(cursor, values);
cursor.close();
}
/**
* Gets SQL expression that represents the given value or NULL depends on the row state.
* @param state Expected row state.
* @param a SQL value.
* @return Expression that represents a if the row state is expected one, and represents NULL
* otherwise.
*/
private static String createStateFilter(int state, String a) {
return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
" THEN " + a + " ELSE NULL END";
}
/**
* Converts values into string array.
* @param args Values converted into string array.
* @return String array.
*/
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;
}
}

View File

@@ -22,10 +22,8 @@ import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

View File

@@ -113,13 +113,14 @@ final class RootScanner {
for (int deviceId : deviceIds) {
try {
final MtpRoot[] roots = mManager.getRoots(deviceId);
mDatabase.startAddingRootDocuments(deviceId);
mDatabase.getMapper().startAddingRootDocuments(deviceId);
try {
if (mDatabase.putRootDocuments(deviceId, mResources, roots)) {
if (mDatabase.getMapper().putRootDocuments(
deviceId, mResources, roots)) {
changed = true;
}
} finally {
if (mDatabase.stopAddingRootDocuments(deviceId)) {
if (mDatabase.getMapper().stopAddingRootDocuments(deviceId)) {
changed = true;
}
}

View File

@@ -40,11 +40,11 @@ public class DocumentLoaderTest extends AndroidTestCase {
@Override
public void setUp() {
mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, new TestResources(), new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, new TestResources(), new MtpRoot[] {
new MtpRoot(0, 0, "Device", "Storage", 1000, 1000, "")
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
mManager = new BlockableTestMtpManager(getContext());
mResolver = new TestContentResolver();
mLoader = new DocumentLoader(mManager, mResolver, mDatabase);

View File

@@ -26,10 +26,8 @@ import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.FileNotFoundException;
import java.util.Set;
import java.util.TreeSet;
import static com.android.mtp.MtpDatabaseInternal.strings;
import static com.android.mtp.MtpDatabase.strings;
@SmallTest
public class MtpDatabaseTest extends AndroidTestCase {
@@ -62,8 +60,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
}
public void testPutRootDocuments() throws Exception {
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 1, "Device", "Storage", 1000, 2000, ""),
new MtpRoot(0, 2, "Device", "Storage", 2000, 4000, ""),
new MtpRoot(0, 3, "Device", "/@#%&<>Storage", 3000, 6000,"")
@@ -154,8 +152,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
}
public void testPutChildDocuments() throws Exception {
mDatabase.startAddingChildDocuments("parentId");
mDatabase.putChildDocuments(0, "parentId", new MtpObjectInfo[] {
mDatabase.getMapper().startAddingChildDocuments("parentId");
mDatabase.getMapper().putChildDocuments(0, "parentId", new MtpObjectInfo[] {
createDocument(100, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
createDocument(101, "image.jpg", MtpConstants.FORMAT_EXIF_JPEG, 2 * 1024 * 1024),
createDocument(102, "music.mp3", MtpConstants.FORMAT_MP3, 3 * 1024 * 1024)
@@ -229,8 +227,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
Root.COLUMN_AVAILABLE_BYTES
};
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 1000, 0, ""),
new MtpRoot(0, 101, "Device", "Storage B", 1001, 0, "")
});
@@ -261,7 +259,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
cursor.close();
}
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
{
final Cursor cursor = mDatabase.queryRootDocuments(columns);
@@ -289,8 +287,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
cursor.close();
}
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage A", 2000, 0, ""),
new MtpRoot(0, 202, "Device", "Storage C", 2002, 0, "")
});
@@ -328,7 +326,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
cursor.close();
}
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
{
final Cursor cursor = mDatabase.queryRootDocuments(columns);
@@ -363,13 +361,13 @@ public class MtpDatabaseTest extends AndroidTestCase {
MtpDatabaseConstants.COLUMN_OBJECT_HANDLE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
};
mDatabase.startAddingChildDocuments("parentId");
mDatabase.putChildDocuments(0, "parentId", new MtpObjectInfo[] {
mDatabase.getMapper().startAddingChildDocuments("parentId");
mDatabase.getMapper().putChildDocuments(0, "parentId", new MtpObjectInfo[] {
createDocument(100, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
createDocument(101, "image.jpg", MtpConstants.FORMAT_EXIF_JPEG, 2 * 1024 * 1024),
createDocument(102, "music.mp3", MtpConstants.FORMAT_MP3, 3 * 1024 * 1024)
});
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
{
final Cursor cursor = mDatabase.queryChildDocuments(columns, "parentId");
@@ -393,8 +391,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
cursor.close();
}
mDatabase.startAddingChildDocuments("parentId");
mDatabase.putChildDocuments(0, "parentId", new MtpObjectInfo[] {
mDatabase.getMapper().startAddingChildDocuments("parentId");
mDatabase.getMapper().putChildDocuments(0, "parentId", new MtpObjectInfo[] {
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
createDocument(203, "video.mp4", MtpConstants.FORMAT_MP4_CONTAINER, 1024),
});
@@ -411,7 +409,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
cursor.close();
}
mDatabase.stopAddingChildDocuments("parentId");
mDatabase.getMapper().stopAddingChildDocuments("parentId");
{
final Cursor cursor = mDatabase.queryChildDocuments(columns, "parentId");
@@ -440,12 +438,12 @@ public class MtpDatabaseTest extends AndroidTestCase {
Root.COLUMN_ROOT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
mDatabase.startAddingRootDocuments(0);
mDatabase.startAddingRootDocuments(1);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().startAddingRootDocuments(1);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage", 0, 0, "")
});
mDatabase.putRootDocuments(1, resources, new MtpRoot[] {
mDatabase.getMapper().putRootDocuments(1, resources, new MtpRoot[] {
new MtpRoot(1, 100, "Device", "Storage", 0, 0, "")
});
@@ -475,18 +473,18 @@ public class MtpDatabaseTest extends AndroidTestCase {
cursor.close();
}
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
mDatabase.startAddingRootDocuments(0);
mDatabase.startAddingRootDocuments(1);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().startAddingRootDocuments(1);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage", 2000, 0, "")
});
mDatabase.putRootDocuments(1, resources, new MtpRoot[] {
mDatabase.getMapper().putRootDocuments(1, resources, new MtpRoot[] {
new MtpRoot(1, 300, "Device", "Storage", 3000, 0, "")
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.stopAddingRootDocuments(1);
mDatabase.getMapper().stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(1);
{
final Cursor cursor = mDatabase.queryRootDocuments(columns);
@@ -521,25 +519,25 @@ public class MtpDatabaseTest extends AndroidTestCase {
MtpDatabaseConstants.COLUMN_OBJECT_HANDLE
};
mDatabase.startAddingChildDocuments("parentId1");
mDatabase.startAddingChildDocuments("parentId2");
mDatabase.putChildDocuments(0, "parentId1", new MtpObjectInfo[] {
mDatabase.getMapper().startAddingChildDocuments("parentId1");
mDatabase.getMapper().startAddingChildDocuments("parentId2");
mDatabase.getMapper().putChildDocuments(0, "parentId1", new MtpObjectInfo[] {
createDocument(100, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
mDatabase.putChildDocuments(0, "parentId2", new MtpObjectInfo[] {
mDatabase.getMapper().putChildDocuments(0, "parentId2", new MtpObjectInfo[] {
createDocument(101, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
mDatabase.startAddingChildDocuments("parentId1");
mDatabase.startAddingChildDocuments("parentId2");
mDatabase.putChildDocuments(0, "parentId1", new MtpObjectInfo[] {
mDatabase.getMapper().startAddingChildDocuments("parentId1");
mDatabase.getMapper().startAddingChildDocuments("parentId2");
mDatabase.getMapper().putChildDocuments(0, "parentId1", new MtpObjectInfo[] {
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
mDatabase.putChildDocuments(0, "parentId2", new MtpObjectInfo[] {
mDatabase.getMapper().putChildDocuments(0, "parentId2", new MtpObjectInfo[] {
createDocument(201, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
mDatabase.stopAddingChildDocuments("parentId1");
mDatabase.getMapper().stopAddingChildDocuments("parentId1");
{
final Cursor cursor = mDatabase.queryChildDocuments(columns, "parentId1");
@@ -570,23 +568,23 @@ public class MtpDatabaseTest extends AndroidTestCase {
Root.COLUMN_AVAILABLE_BYTES
};
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage", 0, 0, ""),
});
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage", 2000, 0, ""),
});
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 300, "Device", "Storage", 3000, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
{
final Cursor cursor = mDatabase.queryRootDocuments(columns);
@@ -618,18 +616,18 @@ public class MtpDatabaseTest extends AndroidTestCase {
Root.COLUMN_AVAILABLE_BYTES
};
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage", 0, 0, ""),
});
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 200, "Device", "Storage", 2000, 0, ""),
new MtpRoot(0, 201, "Device", "Storage", 2001, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
{
final Cursor cursor = mDatabase.queryRootDocuments(columns);
@@ -660,17 +658,17 @@ public class MtpDatabaseTest extends AndroidTestCase {
public void testReplaceExistingRoots() {
// The client code should be able to replace existing rows with new information.
// Add one.
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
// Replace it.
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
{
final String[] columns = new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
@@ -702,24 +700,24 @@ public class MtpDatabaseTest extends AndroidTestCase {
public void testFailToReplaceExisitingUnmappedRoots() {
// The client code should not be able to replace rows before resolving 'unmapped' rows.
// Add one.
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
});
mDatabase.clearMapping();
mDatabase.getMapper().clearMapping();
final Cursor oldCursor = mDatabase.queryRoots(strings(Root.COLUMN_ROOT_ID));
assertEquals(1, oldCursor.getCount());
// Add one.
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 101, "Device", "Storage B", 1000, 1000, ""),
});
// Add one more before resolving unmapped documents.
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 102, "Device", "Storage B", 1000, 1000, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
// Because the roots shares the same name, the roots should have new IDs.
final Cursor newCursor = mDatabase.queryRoots(strings(Root.COLUMN_ROOT_ID));
@@ -735,11 +733,11 @@ public class MtpDatabaseTest extends AndroidTestCase {
}
public void testQueryDocument() {
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
final Cursor cursor = mDatabase.queryDocument("1", strings(Document.COLUMN_DISPLAY_NAME));
assertEquals(1, cursor.getCount());
@@ -749,48 +747,48 @@ public class MtpDatabaseTest extends AndroidTestCase {
}
public void testGetParentId() throws FileNotFoundException {
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
mDatabase.startAddingChildDocuments("1");
mDatabase.putChildDocuments(
mDatabase.getMapper().startAddingChildDocuments("1");
mDatabase.getMapper().putChildDocuments(
0,
"1",
new MtpObjectInfo[] {
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
mDatabase.stopAddingChildDocuments("1");
mDatabase.getMapper().stopAddingChildDocuments("1");
assertEquals("1", mDatabase.getParentId("2"));
}
public void testDeleteDocument() {
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
mDatabase.startAddingChildDocuments("1");
mDatabase.putChildDocuments(
mDatabase.getMapper().startAddingChildDocuments("1");
mDatabase.getMapper().putChildDocuments(
0,
"1",
new MtpObjectInfo[] {
createDocument(200, "dir", MtpConstants.FORMAT_ASSOCIATION, 1024),
});
mDatabase.stopAddingChildDocuments("1");
mDatabase.getMapper().stopAddingChildDocuments("1");
mDatabase.startAddingChildDocuments("2");
mDatabase.putChildDocuments(
mDatabase.getMapper().startAddingChildDocuments("2");
mDatabase.getMapper().putChildDocuments(
0,
"2",
new MtpObjectInfo[] {
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
});
mDatabase.stopAddingChildDocuments("2");
mDatabase.getMapper().stopAddingChildDocuments("2");
mDatabase.deleteDocument("2");
@@ -811,12 +809,12 @@ public class MtpDatabaseTest extends AndroidTestCase {
}
}
public void testPutNewDocument() throws FileNotFoundException {
mDatabase.startAddingRootDocuments(0);
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
public void testPutNewDocument() {
mDatabase.getMapper().startAddingRootDocuments(0);
mDatabase.getMapper().putRootDocuments(0, resources, new MtpRoot[] {
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
});
mDatabase.stopAddingRootDocuments(0);
mDatabase.getMapper().stopAddingRootDocuments(0);
assertEquals(
"2",
@@ -833,13 +831,13 @@ public class MtpDatabaseTest extends AndroidTestCase {
}
// The new document should not be mapped with existing invalidated document.
mDatabase.clearMapping();
mDatabase.startAddingChildDocuments("1");
mDatabase.getMapper().clearMapping();
mDatabase.getMapper().startAddingChildDocuments("1");
mDatabase.putNewDocument(
0,
"1",
createDocument(201, "note.txt", MtpConstants.FORMAT_TEXT, 1024));
mDatabase.stopAddingChildDocuments("1");
mDatabase.getMapper().stopAddingChildDocuments("1");
{
final Cursor cursor =

View File

@@ -29,7 +29,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import static com.android.mtp.MtpDatabaseInternal.strings;
import static com.android.mtp.MtpDatabase.strings;
@SmallTest
public class MtpDocumentsProviderTest extends AndroidTestCase {