Extract mapping operations to partial class of MtpDatabase.
Change-Id: I5fda6d5754206528ad5953bcc179a0ffe0b4982d
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
513
packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java
Normal file
513
packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user