Merge "Add new methods to MtpDatabase."
This commit is contained in:
committed by
Android (Google) Code Review
commit
74bfa27199
@@ -78,6 +78,12 @@ class DocumentLoader {
|
||||
if (parentHandle == CursorHelper.DUMMY_HANDLE_FOR_ROOT) {
|
||||
parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
|
||||
}
|
||||
// TODO: Handle nit race around here.
|
||||
// 1. getObjectHandles.
|
||||
// 2. putNewDocument.
|
||||
// 3. startAddingChildDocuemnts.
|
||||
// 4. stopAddingChildDocuments - It removes the new document added at the step 2,
|
||||
// because it is not updated between start/stopAddingChildDocuments.
|
||||
task = new LoaderTask(mDatabase, parent, mMtpManager.getObjectHandles(
|
||||
parent.mDeviceId, parent.mStorageId, parentHandle));
|
||||
task.fillDocuments(loadDocuments(
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.provider.DocumentsContract.Root;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -133,6 +134,14 @@ class MtpDatabase {
|
||||
return mDatabase.queryChildDocuments(newColumnNames, parentDocumentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MtpDatabaseInternal#queryDocument}
|
||||
*/
|
||||
@VisibleForTesting
|
||||
Cursor queryDocument(String documentId, String[] projection) {
|
||||
return mDatabase.queryDocument(documentId, projection);
|
||||
}
|
||||
|
||||
Identifier createIdentifier(String parentDocumentId) {
|
||||
return mDatabase.createIdentifier(parentDocumentId);
|
||||
}
|
||||
@@ -144,6 +153,35 @@ class MtpDatabase {
|
||||
mDatabase.removeDeviceRows(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MtpDatabaseInternal#getParentId}
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String getParentId(String documentId) throws FileNotFoundException {
|
||||
return mDatabase.getParentId(documentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MtpDatabaseInternal#deleteDocument}
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void deleteDocument(String documentId) {
|
||||
mDatabase.deleteDocument(documentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MtpDatabaseInternal#putNewDocument}
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String putNewDocument(int deviceId, String parentDocumentId, MtpObjectInfo info)
|
||||
throws FileNotFoundException {
|
||||
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.
|
||||
@@ -186,7 +224,11 @@ class MtpDatabase {
|
||||
try {
|
||||
final boolean heuristic;
|
||||
final String mapColumn;
|
||||
switch (mMappingMode.get(getRootDocumentsMappingStateKey(deviceId))) {
|
||||
final String key = getRootDocumentsMappingStateKey(deviceId);
|
||||
if (!mMappingMode.containsKey(key)) {
|
||||
throw new IllegalStateException("startAddingRootDocuments has not been called.");
|
||||
}
|
||||
switch (mMappingMode.get(key)) {
|
||||
case MAP_BY_MTP_IDENTIFIER:
|
||||
heuristic = false;
|
||||
mapColumn = COLUMN_STORAGE_ID;
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -117,14 +118,87 @@ class MtpDatabaseInternal {
|
||||
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.
|
||||
@@ -197,7 +271,7 @@ class MtpDatabaseInternal {
|
||||
mDatabase.beginTransaction();
|
||||
try {
|
||||
// Delete all pending rows.
|
||||
deleteDocumentsAndRoots(
|
||||
deleteDocumentsAndRootsRecursively(
|
||||
selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));
|
||||
|
||||
// Set all documents as invalidated.
|
||||
@@ -366,14 +440,14 @@ class MtpDatabaseInternal {
|
||||
}
|
||||
|
||||
// Delete 'pending' row.
|
||||
deleteDocumentsAndRoots(SELECTION_DOCUMENT_ID, new String[] { pendingId });
|
||||
deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, new String[] { pendingId });
|
||||
}
|
||||
mergingCursor.close();
|
||||
|
||||
boolean changed = false;
|
||||
|
||||
// Delete all invalidated rows that cannot be mapped.
|
||||
if (deleteDocumentsAndRoots(
|
||||
if (deleteDocumentsAndRootsRecursively(
|
||||
COLUMN_ROW_STATE + " = ? AND " + selection,
|
||||
strings(ROW_STATE_INVALIDATED, arg))) {
|
||||
changed = true;
|
||||
@@ -407,7 +481,7 @@ class MtpDatabaseInternal {
|
||||
void clearMapping() {
|
||||
mDatabase.beginTransaction();
|
||||
try {
|
||||
deleteDocumentsAndRoots(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
|
||||
deleteDocumentsAndRootsRecursively(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
|
||||
final ContentValues values = new ContentValues();
|
||||
values.putNull(COLUMN_OBJECT_HANDLE);
|
||||
values.putNull(COLUMN_STORAGE_ID);
|
||||
@@ -446,6 +520,39 @@ class MtpDatabaseInternal {
|
||||
* @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 {
|
||||
@@ -464,6 +571,8 @@ class MtpDatabaseInternal {
|
||||
args);
|
||||
deleted += mDatabase.delete(TABLE_DOCUMENTS, selection, args);
|
||||
mDatabase.setTransactionSuccessful();
|
||||
// TODO Remove child.
|
||||
// TODO Remove mappingState.
|
||||
return deleted != 0;
|
||||
} finally {
|
||||
mDatabase.endTransaction();
|
||||
@@ -505,7 +614,7 @@ class MtpDatabaseInternal {
|
||||
* @param args Values converted into string array.
|
||||
* @return String array.
|
||||
*/
|
||||
private static String[] strings(Object... args) {
|
||||
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]);
|
||||
|
||||
@@ -20,10 +20,17 @@ import android.database.Cursor;
|
||||
import android.mtp.MtpConstants;
|
||||
import android.mtp.MtpObjectInfo;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
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;
|
||||
|
||||
@SmallTest
|
||||
public class MtpDatabaseTest extends AndroidTestCase {
|
||||
private final String[] COLUMN_NAMES = new String[] {
|
||||
@@ -692,7 +699,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
public void _testFailToReplaceExisitingUnmappedRoots() {
|
||||
public void testFailToReplaceExisitingUnmappedRoots() {
|
||||
// The client code should not be able to replace rows before resolving 'unmapped' rows.
|
||||
// Add one.
|
||||
mDatabase.startAddingRootDocuments(0);
|
||||
@@ -700,18 +707,147 @@ public class MtpDatabaseTest extends AndroidTestCase {
|
||||
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
|
||||
});
|
||||
mDatabase.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[] {
|
||||
new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
|
||||
new MtpRoot(0, 101, "Device", "Storage B", 1000, 1000, ""),
|
||||
});
|
||||
// Add one more before resolving unmapped documents.
|
||||
try {
|
||||
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
|
||||
new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
|
||||
});
|
||||
fail();
|
||||
} catch (Throwable e) {
|
||||
assertTrue(e instanceof Error);
|
||||
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
|
||||
new MtpRoot(0, 102, "Device", "Storage B", 1000, 1000, ""),
|
||||
});
|
||||
mDatabase.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));
|
||||
assertEquals(2, newCursor.getCount());
|
||||
oldCursor.moveToNext();
|
||||
newCursor.moveToNext();
|
||||
assertFalse(oldCursor.getString(0).equals(newCursor.getString(0)));
|
||||
newCursor.moveToNext();
|
||||
assertFalse(oldCursor.getString(0).equals(newCursor.getString(0)));
|
||||
|
||||
oldCursor.close();
|
||||
newCursor.close();
|
||||
}
|
||||
|
||||
public void testQueryDocument() {
|
||||
mDatabase.startAddingRootDocuments(0);
|
||||
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
|
||||
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
|
||||
});
|
||||
mDatabase.stopAddingRootDocuments(0);
|
||||
|
||||
final Cursor cursor = mDatabase.queryDocument("1", strings(Document.COLUMN_DISPLAY_NAME));
|
||||
assertEquals(1, cursor.getCount());
|
||||
cursor.moveToNext();
|
||||
assertEquals("Device Storage A", cursor.getString(0));
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
public void testGetParentId() throws FileNotFoundException {
|
||||
mDatabase.startAddingRootDocuments(0);
|
||||
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
|
||||
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
|
||||
});
|
||||
mDatabase.stopAddingRootDocuments(0);
|
||||
|
||||
mDatabase.startAddingChildDocuments("1");
|
||||
mDatabase.putChildDocuments(
|
||||
0,
|
||||
"1",
|
||||
new MtpObjectInfo[] {
|
||||
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
|
||||
});
|
||||
mDatabase.stopAddingChildDocuments("1");
|
||||
|
||||
assertEquals("1", mDatabase.getParentId("2"));
|
||||
}
|
||||
|
||||
public void testDeleteDocument() {
|
||||
mDatabase.startAddingRootDocuments(0);
|
||||
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
|
||||
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
|
||||
});
|
||||
mDatabase.stopAddingRootDocuments(0);
|
||||
|
||||
mDatabase.startAddingChildDocuments("1");
|
||||
mDatabase.putChildDocuments(
|
||||
0,
|
||||
"1",
|
||||
new MtpObjectInfo[] {
|
||||
createDocument(200, "dir", MtpConstants.FORMAT_ASSOCIATION, 1024),
|
||||
});
|
||||
mDatabase.stopAddingChildDocuments("1");
|
||||
|
||||
mDatabase.startAddingChildDocuments("2");
|
||||
mDatabase.putChildDocuments(
|
||||
0,
|
||||
"2",
|
||||
new MtpObjectInfo[] {
|
||||
createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
|
||||
});
|
||||
mDatabase.stopAddingChildDocuments("2");
|
||||
|
||||
mDatabase.deleteDocument("2");
|
||||
|
||||
{
|
||||
// Do not query deleted documents.
|
||||
final Cursor cursor =
|
||||
mDatabase.queryChildDocuments(strings(Document.COLUMN_DOCUMENT_ID), "1");
|
||||
assertEquals(0, cursor.getCount());
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
{
|
||||
// Child document should be deleted also.
|
||||
final Cursor cursor =
|
||||
mDatabase.queryDocument("3", strings(Document.COLUMN_DOCUMENT_ID));
|
||||
assertEquals(0, cursor.getCount());
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void testPutNewDocument() throws FileNotFoundException {
|
||||
mDatabase.startAddingRootDocuments(0);
|
||||
mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
|
||||
new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
|
||||
});
|
||||
mDatabase.stopAddingRootDocuments(0);
|
||||
|
||||
assertEquals(
|
||||
"2",
|
||||
mDatabase.putNewDocument(
|
||||
0, "1", createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024)));
|
||||
|
||||
{
|
||||
final Cursor cursor =
|
||||
mDatabase.queryChildDocuments(strings(Document.COLUMN_DOCUMENT_ID), "1");
|
||||
assertEquals(1, cursor.getCount());
|
||||
cursor.moveToNext();
|
||||
assertEquals("2", cursor.getString(0));
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
// The new document should not be mapped with existing invalidated document.
|
||||
mDatabase.clearMapping();
|
||||
mDatabase.startAddingChildDocuments("1");
|
||||
mDatabase.putNewDocument(
|
||||
0,
|
||||
"1",
|
||||
createDocument(201, "note.txt", MtpConstants.FORMAT_TEXT, 1024));
|
||||
mDatabase.stopAddingChildDocuments("1");
|
||||
|
||||
{
|
||||
final Cursor cursor =
|
||||
mDatabase.queryChildDocuments(strings(Document.COLUMN_DOCUMENT_ID), "1");
|
||||
assertEquals(1, cursor.getCount());
|
||||
cursor.moveToNext();
|
||||
assertEquals("3", cursor.getString(0));
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user