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,