Add new methods to MtpDatabase.

* deleteDocument
 * getParentId
 * queryDocument
 * putNewDocument

BUG=25756881

Change-Id: Ie223b37e04586b3e2b0448d09e14562fedbb652a
This commit is contained in:
Daichi Hirono
2015-11-19 10:19:52 +09:00
parent e1d57710fb
commit 49f920fbde
4 changed files with 308 additions and 15 deletions

View File

@@ -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(

View File

@@ -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;

View File

@@ -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]);

View File

@@ -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();
}
}
}