From 53f5af3f2ba1328d301a0f8a4ae3f574ccc5da65 Mon Sep 17 00:00:00 2001 From: Daichi Hirono Date: Sun, 7 Feb 2016 15:17:16 +0900 Subject: [PATCH] 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 --- .../src/com/android/mtp/Mapper.java | 67 +++++++++----- .../src/com/android/mtp/MtpDatabase.java | 92 ++++++++++++++++--- .../com/android/mtp/MtpDatabaseConstants.java | 10 +- .../src/com/android/mtp/MtpDatabaseTest.java | 73 +++------------ 4 files changed, 147 insertions(+), 95 deletions(-) diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java b/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java index cf1e1f61be027..897272907a734 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java +++ b/packages/MtpDocumentsProvider/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.database.sqlite.SQLiteDatabase; 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.io.FileNotFoundException; 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 @@ class Mapper { 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 @@ class Mapper { 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 @@ class Mapper { 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 @@ class Mapper { 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 @@ class Mapper { 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 @@ class Mapper { 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/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java index 8a3ebefa127ff..c456be93d5a39 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java @@ -219,7 +219,7 @@ class MtpDatabase { 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 @@ class MtpDatabase { "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 @@ class MtpDatabase { 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 @@ class MtpDatabase { try { while (cursor.moveToNext()) { if (deleteDocumentsAndRootsRecursively( - COLUMN_PARENT_DOCUMENT_ID + "=?", + COLUMN_PARENT_DOCUMENT_ID + " = ?", strings(cursor.getString(0)))) { changed = true; } @@ -456,7 +447,43 @@ class MtpDatabase { } } - 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 @@ class MtpDatabase { } } + 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 @@ class MtpDatabase { @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/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java index 33687cbc15002..cb076af118b40 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java @@ -30,7 +30,7 @@ import java.util.Map; * 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; @@ -77,6 +77,12 @@ class MtpDatabaseConstants { */ 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. */ @@ -113,7 +119,7 @@ class MtpDatabaseConstants { "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/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java index ea52957771ec7..9005152a5ae94 100644 --- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java +++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java @@ -95,7 +95,8 @@ public class MtpDatabaseTest extends AndroidTestCase { 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 @@ public class MtpDatabaseTest extends AndroidTestCase { 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 @@ public class MtpDatabaseTest extends AndroidTestCase { { 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 @@ public class MtpDatabaseTest extends AndroidTestCase { 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 @@ public class MtpDatabaseTest extends AndroidTestCase { 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 @@ public class MtpDatabaseTest extends AndroidTestCase { 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 class MtpDatabaseTest extends AndroidTestCase { } 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 class MtpDatabaseTest extends AndroidTestCase { 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 @@ public class MtpDatabaseTest extends AndroidTestCase { // The new document should not be mapped with existing invalidated document. mDatabase.getMapper().clearMapping(); + addTestDevice(); + addTestStorage("1"); + mDatabase.getMapper().startAddingDocuments("2"); mDatabase.putNewDocument( 0,