Keep metadata of documents as disconnected status after the device is
disconnected.
To restore Document IDs when the device is reconnected, we need to keep
the metadata in database so that we can use it as hint to remap document
ID with new MTP IDs.
BUG=26212981
Change-Id: Idcc93c41c09d082a709281022c56188dabc80515
(cherry picked from commit 53f5af3f2ba1328d301a0f8a4ae3f574ccc5da65)
diff --git a/src/com/android/mtp/Mapper.java b/src/com/android/mtp/Mapper.java
index cf1e1f6..8972729 100644
--- a/src/com/android/mtp/Mapper.java
+++ b/src/com/android/mtp/Mapper.java
@@ -16,8 +16,6 @@
package com.android.mtp;
-import static com.android.mtp.MtpDatabaseConstants.*;
-
import android.annotation.Nullable;
import android.content.ContentValues;
import android.database.Cursor;
@@ -26,6 +24,7 @@
import android.mtp.MtpObjectInfo;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
+import android.util.Log;
import com.android.internal.util.Preconditions;
@@ -33,6 +32,7 @@
import java.util.HashMap;
import java.util.Map;
+import static com.android.mtp.MtpDatabaseConstants.*;
import static com.android.mtp.MtpDatabase.strings;
/**
@@ -75,7 +75,7 @@
extraValuesList,
COLUMN_PARENT_DOCUMENT_ID + " IS NULL",
EMPTY_ARGS,
- COLUMN_DEVICE_ID);
+ Document.COLUMN_DISPLAY_NAME);
database.setTransactionSuccessful();
return changed;
} finally {
@@ -172,13 +172,16 @@
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction();
try {
- 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();
+ // Disconnect all device rows.
+ try {
+ startAddingDocuments(null);
+ stopAddingDocuments(null);
+ } catch (FileNotFoundException exception) {
+ Log.e(MtpDocumentsProvider.TAG, "Unexpected FileNotFoundException.", exception);
+ throw new RuntimeException(exception);
+ }
+ database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
@@ -210,16 +213,20 @@
getParentOrHaltMapping(parentDocumentId);
Preconditions.checkState(!mMappingMode.containsKey(parentDocumentId));
- // Set all documents as invalidated.
+ // Set all valid documents as invalidated.
final ContentValues values = new ContentValues();
values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
- database.update(TABLE_DOCUMENTS, values, selection, args);
+ database.update(
+ TABLE_DOCUMENTS,
+ values,
+ selection + " AND " + COLUMN_ROW_STATE + " = ?",
+ DatabaseUtils.appendSelectionArgs(args, strings(ROW_STATE_VALID)));
// 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",
+ selection + " AND " + COLUMN_DEVICE_ID + " IS NULL",
args) > 0;
database.setTransactionSuccessful();
mMappingMode.put(
@@ -270,11 +277,13 @@
TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID),
selection + " AND " +
- COLUMN_ROW_STATE + "=? AND " +
+ COLUMN_ROW_STATE + " IN (?, ?) AND " +
mappingKey + "=?",
DatabaseUtils.appendSelectionArgs(
args,
- strings(ROW_STATE_INVALIDATED, values.getAsString(mappingKey))),
+ strings(ROW_STATE_INVALIDATED,
+ ROW_STATE_DISCONNECTED,
+ values.getAsString(mappingKey))),
null,
null,
null,
@@ -335,16 +344,27 @@
final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction();
try {
- getParentOrHaltMapping(parentId);
+ final Identifier parentIdentifier = getParentOrHaltMapping(parentId);
Preconditions.checkState(mMappingMode.containsKey(parentId));
mMappingMode.remove(parentId);
boolean changed = false;
- // Delete all invalidated rows that cannot be mapped.
- if (mDatabase.deleteDocumentsAndRootsRecursively(
- COLUMN_ROW_STATE + " = ? AND " + selection,
- DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_INVALIDATED), args))) {
- changed = true;
+ // Delete/disconnect all invalidated rows that cannot be mapped.
+ final boolean keepUnmatchedDocument =
+ parentIdentifier == null ||
+ parentIdentifier.mDocumentType == DOCUMENT_TYPE_DEVICE;
+ if (keepUnmatchedDocument) {
+ if (mDatabase.disconnectDocumentsRecursively(
+ COLUMN_ROW_STATE + " = ? AND " + selection,
+ DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_INVALIDATED), args))) {
+ changed = true;
+ }
+ } else {
+ if (mDatabase.deleteDocumentsAndRootsRecursively(
+ COLUMN_ROW_STATE + " = ? AND " + selection,
+ DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_INVALIDATED), args))) {
+ changed = true;
+ }
}
database.setTransactionSuccessful();
@@ -368,7 +388,12 @@
return null;
}
try {
- return mDatabase.createIdentifier(parentId);
+ final Identifier identifier = mDatabase.createIdentifier(parentId);
+ if (mDatabase.getRowState(parentId) == ROW_STATE_DISCONNECTED) {
+ throw new FileNotFoundException(
+ "document: " + parentId + " is in disconnected device.");
+ }
+ return identifier;
} catch (FileNotFoundException error) {
mMappingMode.remove(parentId);
throw error;
diff --git a/src/com/android/mtp/MtpDatabase.java b/src/com/android/mtp/MtpDatabase.java
index 8a3ebef..c456be9 100644
--- a/src/com/android/mtp/MtpDatabase.java
+++ b/src/com/android/mtp/MtpDatabase.java
@@ -219,7 +219,7 @@
return mDatabase.query(
TABLE_DOCUMENTS,
columnNames,
- COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + "=?",
+ COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_DOCUMENT_TYPE + " = ?",
strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE),
null,
null,
@@ -294,15 +294,6 @@
"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));
- }
-
@Nullable String getDocumentIdForDevice(int deviceId) {
final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS,
@@ -344,7 +335,7 @@
if (cursor.moveToNext()) {
return createIdentifier(cursor.getString(0));
} else {
- throw new FileNotFoundException("Cannot find a row having ID=" + documentId);
+ throw new FileNotFoundException("Cannot find a row having ID = " + documentId);
}
} finally {
cursor.close();
@@ -438,7 +429,7 @@
try {
while (cursor.moveToNext()) {
if (deleteDocumentsAndRootsRecursively(
- COLUMN_PARENT_DOCUMENT_ID + "=?",
+ COLUMN_PARENT_DOCUMENT_ID + " = ?",
strings(cursor.getString(0)))) {
changed = true;
}
@@ -456,7 +447,43 @@
}
}
- private boolean deleteDocumentsAndRoots(String selection, String[] args) {
+ /**
+ * Marks the documents and their child as disconnected documents.
+ * @param selection
+ * @param args
+ * @return True if at least one row is updated.
+ */
+ boolean disconnectDocumentsRecursively(String selection, String[] args) {
+ mDatabase.beginTransaction();
+ try {
+ boolean changed = false;
+ try (final Cursor cursor = mDatabase.query(
+ TABLE_DOCUMENTS,
+ strings(Document.COLUMN_DOCUMENT_ID),
+ selection,
+ args,
+ null,
+ null,
+ null)) {
+ while (cursor.moveToNext()) {
+ if (disconnectDocumentsRecursively(
+ COLUMN_PARENT_DOCUMENT_ID + " = ?",
+ strings(cursor.getString(0)))) {
+ changed = true;
+ }
+ }
+ }
+ if (disconnectDocuments(selection, args)) {
+ changed = true;
+ }
+ mDatabase.setTransactionSuccessful();
+ return changed;
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ boolean deleteDocumentsAndRoots(String selection, String[] args) {
mDatabase.beginTransaction();
try {
int deleted = 0;
@@ -481,6 +508,39 @@
}
}
+ boolean disconnectDocuments(String selection, String[] args) {
+ mDatabase.beginTransaction();
+ try {
+ final ContentValues values = new ContentValues();
+ values.put(COLUMN_ROW_STATE, ROW_STATE_DISCONNECTED);
+ values.putNull(COLUMN_DEVICE_ID);
+ values.putNull(COLUMN_STORAGE_ID);
+ values.putNull(COLUMN_OBJECT_HANDLE);
+ final boolean updated = mDatabase.update(TABLE_DOCUMENTS, values, selection, args) != 0;
+ mDatabase.setTransactionSuccessful();
+ return updated;
+ } finally {
+ mDatabase.endTransaction();
+ }
+ }
+
+ int getRowState(String documentId) throws FileNotFoundException {
+ try (final Cursor cursor = mDatabase.query(
+ TABLE_DOCUMENTS,
+ strings(COLUMN_ROW_STATE),
+ SELECTION_DOCUMENT_ID,
+ strings(documentId),
+ null,
+ null,
+ null)) {
+ if (cursor.getCount() == 0) {
+ throw new FileNotFoundException();
+ }
+ cursor.moveToNext();
+ return cursor.getInt(0);
+ }
+ }
+
private static class OpenHelper extends SQLiteOpenHelper {
public OpenHelper(Context context, int flags) {
super(context,
@@ -497,6 +557,12 @@
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion == 1) {
+ db.execSQL("DROP TABLE " + TABLE_DOCUMENTS);
+ db.execSQL("DROP TABLE " + TABLE_ROOT_EXTRA);
+ onCreate(db);
+ return;
+ }
throw new UnsupportedOperationException();
}
}
diff --git a/src/com/android/mtp/MtpDatabaseConstants.java b/src/com/android/mtp/MtpDatabaseConstants.java
index 33687cb..cb076af 100644
--- a/src/com/android/mtp/MtpDatabaseConstants.java
+++ b/src/com/android/mtp/MtpDatabaseConstants.java
@@ -30,7 +30,7 @@
* Class containing MtpDatabase constants.
*/
class MtpDatabaseConstants {
- static final int DATABASE_VERSION = 1;
+ static final int DATABASE_VERSION = 2;
static final String DATABASE_NAME = "database";
static final int FLAG_DATABASE_IN_MEMORY = 1;
@@ -78,6 +78,12 @@
static final int ROW_STATE_INVALIDATED = 1;
/**
+ * The documents are of device/storage that are disconnected now. The documents are invisible
+ * but their document ID will be reuse when the device/storage is connected again.
+ */
+ static final int ROW_STATE_DISCONNECTED = 2;
+
+ /**
* Mapping mode that uses MTP identifier to find corresponding rows.
*/
static final int MAP_BY_MTP_IDENTIFIER = 0;
@@ -113,7 +119,7 @@
"CREATE TABLE " + TABLE_DOCUMENTS + " (" +
Document.COLUMN_DOCUMENT_ID +
" INTEGER PRIMARY KEY AUTOINCREMENT," +
- COLUMN_DEVICE_ID + " INTEGER NOT NULL," +
+ COLUMN_DEVICE_ID + " INTEGER," +
COLUMN_STORAGE_ID + " INTEGER," +
COLUMN_OBJECT_HANDLE + " INTEGER," +
COLUMN_PARENT_DOCUMENT_ID + " INTEGER," +
diff --git a/tests/src/com/android/mtp/MtpDatabaseTest.java b/tests/src/com/android/mtp/MtpDatabaseTest.java
index ea52957..9005152 100644
--- a/tests/src/com/android/mtp/MtpDatabaseTest.java
+++ b/tests/src/com/android/mtp/MtpDatabaseTest.java
@@ -95,7 +95,8 @@
assertEquals(0, getInt(cursor, COLUMN_DEVICE_ID));
assertEquals(1, getInt(cursor, COLUMN_STORAGE_ID));
assertTrue(isNull(cursor, COLUMN_OBJECT_HANDLE));
- assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, getString(cursor, COLUMN_MIME_TYPE));
+ assertEquals(
+ DocumentsContract.Document.MIME_TYPE_DIR, getString(cursor, COLUMN_MIME_TYPE));
assertEquals("Storage", getString(cursor, COLUMN_DISPLAY_NAME));
assertTrue(isNull(cursor, COLUMN_SUMMARY));
assertTrue(isNull(cursor, COLUMN_LAST_MODIFIED));
@@ -103,7 +104,8 @@
assertEquals(0, getInt(cursor, COLUMN_FLAGS));
assertEquals(1000, getInt(cursor, COLUMN_SIZE));
assertEquals(
- MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE, getInt(cursor, COLUMN_DOCUMENT_TYPE));
+ MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE,
+ getInt(cursor, COLUMN_DOCUMENT_TYPE));
cursor.close();
}
@@ -296,15 +298,7 @@
{
final Cursor cursor = mDatabase.queryRootDocuments(columns);
- assertEquals(2, cursor.getCount());
- cursor.moveToNext();
- assertEquals(2, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertTrue(isNull(cursor, COLUMN_STORAGE_ID));
- assertEquals("Storage A", getString(cursor, COLUMN_DISPLAY_NAME));
- cursor.moveToNext();
- assertEquals(3, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertTrue(isNull(cursor, COLUMN_STORAGE_ID));
- assertEquals("Storage B", getString(cursor, COLUMN_DISPLAY_NAME));
+ assertEquals(0, cursor.getCount());
cursor.close();
}
@@ -314,28 +308,10 @@
new MtpRoot(0, 200, "Storage A", 2000, 0, ""),
new MtpRoot(0, 202, "Storage C", 2002, 0, "")
});
-
- {
- final Cursor cursor = mDatabase.queryRootDocuments(columns);
- assertEquals(3, cursor.getCount());
- cursor.moveToNext();
- assertEquals(2, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertEquals(200, getInt(cursor, COLUMN_STORAGE_ID));
- assertEquals("Storage A", getString(cursor, COLUMN_DISPLAY_NAME));
- cursor.moveToNext();
- assertEquals(3, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertTrue(isNull(cursor, COLUMN_STORAGE_ID));
- assertEquals("Storage B", getString(cursor, COLUMN_DISPLAY_NAME));
- cursor.moveToNext();
- assertEquals(4, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertEquals(202, getInt(cursor, COLUMN_STORAGE_ID));
- assertEquals("Storage C", getString(cursor, COLUMN_DISPLAY_NAME));
- cursor.close();
- }
-
mDatabase.getMapper().stopAddingDocuments("1");
{
+ // After compeleting mapping, Storage A can be obtained with new storage ID.
final Cursor cursor = mDatabase.queryRootDocuments(columns);
assertEquals(2, cursor.getCount());
cursor.moveToNext();
@@ -372,24 +348,9 @@
addTestStorage("1");
{
+ // Don't return objects that lost MTP object handles.
final Cursor cursor = mDatabase.queryChildDocuments(columns, "2");
- assertEquals(3, cursor.getCount());
-
- cursor.moveToNext();
- assertEquals(3, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertTrue(isNull(cursor, COLUMN_OBJECT_HANDLE));
- assertEquals("note.txt", getString(cursor, COLUMN_DISPLAY_NAME));
-
- cursor.moveToNext();
- assertEquals(4, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertTrue(isNull(cursor, COLUMN_OBJECT_HANDLE));
- assertEquals("image.jpg", getString(cursor, COLUMN_DISPLAY_NAME));
-
- cursor.moveToNext();
- assertEquals(5, getInt(cursor, COLUMN_DOCUMENT_ID));
- assertTrue(isNull(cursor, COLUMN_OBJECT_HANDLE));
- assertEquals("music.mp3", getString(cursor, COLUMN_DISPLAY_NAME));
-
+ assertEquals(0, cursor.getCount());
cursor.close();
}
@@ -598,10 +559,7 @@
Root.COLUMN_AVAILABLE_BYTES
};
- mDatabase.getMapper().startAddingDocuments(null);
- mDatabase.getMapper().putDeviceDocument(new MtpDeviceRecord(
- 0, "Device", false, new MtpRoot[0], null, null));
- mDatabase.getMapper().stopAddingDocuments(null);
+ addTestDevice();
mDatabase.getMapper().startAddingDocuments("1");
mDatabase.getMapper().putStorageDocuments("1", new MtpRoot[] {
@@ -701,10 +659,7 @@
}
public void testReplaceExistingRoots() throws Exception {
- mDatabase.getMapper().startAddingDocuments(null);
- mDatabase.getMapper().putDeviceDocument(new MtpDeviceRecord(
- 0, "Device", true, new MtpRoot[0], null, null));
- mDatabase.getMapper().stopAddingDocuments(null);
+ addTestDevice();
// The client code should be able to replace existing rows with new information.
// Add one.
@@ -802,10 +757,7 @@
public void testQueryRoots() throws Exception {
// Add device document.
- mDatabase.getMapper().startAddingDocuments(null);
- mDatabase.getMapper().putDeviceDocument(new MtpDeviceRecord(
- 0, "Device", false, new MtpRoot[0], null, null));
- mDatabase.getMapper().stopAddingDocuments(null);
+ addTestDevice();
// It the device does not have storages, it shows a device root.
{
@@ -930,6 +882,9 @@
// The new document should not be mapped with existing invalidated document.
mDatabase.getMapper().clearMapping();
+ addTestDevice();
+ addTestStorage("1");
+
mDatabase.getMapper().startAddingDocuments("2");
mDatabase.putNewDocument(
0,