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 53f5af3f2b)
This commit is contained in:
Daichi Hirono
2016-02-07 15:17:16 +09:00
parent 66a320d368
commit 8e87364a67
4 changed files with 147 additions and 95 deletions

View File

@@ -16,8 +16,6 @@
package com.android.mtp; package com.android.mtp;
import static com.android.mtp.MtpDatabaseConstants.*;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.content.ContentValues; import android.content.ContentValues;
import android.database.Cursor; import android.database.Cursor;
@@ -26,6 +24,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.mtp.MtpObjectInfo; import android.mtp.MtpObjectInfo;
import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root; import android.provider.DocumentsContract.Root;
import android.util.Log;
import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions;
@@ -33,6 +32,7 @@ import java.io.FileNotFoundException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static com.android.mtp.MtpDatabaseConstants.*;
import static com.android.mtp.MtpDatabase.strings; import static com.android.mtp.MtpDatabase.strings;
/** /**
@@ -75,7 +75,7 @@ class Mapper {
extraValuesList, extraValuesList,
COLUMN_PARENT_DOCUMENT_ID + " IS NULL", COLUMN_PARENT_DOCUMENT_ID + " IS NULL",
EMPTY_ARGS, EMPTY_ARGS,
COLUMN_DEVICE_ID); Document.COLUMN_DISPLAY_NAME);
database.setTransactionSuccessful(); database.setTransactionSuccessful();
return changed; return changed;
} finally { } finally {
@@ -172,13 +172,16 @@ class Mapper {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase(); final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction(); database.beginTransaction();
try { 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(); 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 { } finally {
database.endTransaction(); database.endTransaction();
} }
@@ -210,16 +213,20 @@ class Mapper {
getParentOrHaltMapping(parentDocumentId); getParentOrHaltMapping(parentDocumentId);
Preconditions.checkState(!mMappingMode.containsKey(parentDocumentId)); Preconditions.checkState(!mMappingMode.containsKey(parentDocumentId));
// Set all documents as invalidated. // Set all valid documents as invalidated.
final ContentValues values = new ContentValues(); final ContentValues values = new ContentValues();
values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED); 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. // If we have rows that does not have MTP identifier, do heuristic mapping by name.
final boolean useNameForResolving = DatabaseUtils.queryNumEntries( final boolean useNameForResolving = DatabaseUtils.queryNumEntries(
database, database,
TABLE_DOCUMENTS, TABLE_DOCUMENTS,
selection + " AND " + COLUMN_STORAGE_ID + " IS NULL", selection + " AND " + COLUMN_DEVICE_ID + " IS NULL",
args) > 0; args) > 0;
database.setTransactionSuccessful(); database.setTransactionSuccessful();
mMappingMode.put( mMappingMode.put(
@@ -270,11 +277,13 @@ class Mapper {
TABLE_DOCUMENTS, TABLE_DOCUMENTS,
strings(Document.COLUMN_DOCUMENT_ID), strings(Document.COLUMN_DOCUMENT_ID),
selection + " AND " + selection + " AND " +
COLUMN_ROW_STATE + "=? AND " + COLUMN_ROW_STATE + " IN (?, ?) AND " +
mappingKey + "=?", mappingKey + "=?",
DatabaseUtils.appendSelectionArgs( DatabaseUtils.appendSelectionArgs(
args, args,
strings(ROW_STATE_INVALIDATED, values.getAsString(mappingKey))), strings(ROW_STATE_INVALIDATED,
ROW_STATE_DISCONNECTED,
values.getAsString(mappingKey))),
null, null,
null, null,
null, null,
@@ -335,16 +344,27 @@ class Mapper {
final SQLiteDatabase database = mDatabase.getSQLiteDatabase(); final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
database.beginTransaction(); database.beginTransaction();
try { try {
getParentOrHaltMapping(parentId); final Identifier parentIdentifier = getParentOrHaltMapping(parentId);
Preconditions.checkState(mMappingMode.containsKey(parentId)); Preconditions.checkState(mMappingMode.containsKey(parentId));
mMappingMode.remove(parentId); mMappingMode.remove(parentId);
boolean changed = false; boolean changed = false;
// Delete all invalidated rows that cannot be mapped. // Delete/disconnect all invalidated rows that cannot be mapped.
if (mDatabase.deleteDocumentsAndRootsRecursively( final boolean keepUnmatchedDocument =
COLUMN_ROW_STATE + " = ? AND " + selection, parentIdentifier == null ||
DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_INVALIDATED), args))) { parentIdentifier.mDocumentType == DOCUMENT_TYPE_DEVICE;
changed = true; 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(); database.setTransactionSuccessful();
@@ -368,7 +388,12 @@ class Mapper {
return null; return null;
} }
try { 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) { } catch (FileNotFoundException error) {
mMappingMode.remove(parentId); mMappingMode.remove(parentId);
throw error; throw error;

View File

@@ -219,7 +219,7 @@ class MtpDatabase {
return mDatabase.query( return mDatabase.query(
TABLE_DOCUMENTS, TABLE_DOCUMENTS,
columnNames, 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), strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, DOCUMENT_TYPE_STORAGE),
null, null,
null, null,
@@ -294,15 +294,6 @@ class MtpDatabase {
"1"); "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) { @Nullable String getDocumentIdForDevice(int deviceId) {
final Cursor cursor = mDatabase.query( final Cursor cursor = mDatabase.query(
TABLE_DOCUMENTS, TABLE_DOCUMENTS,
@@ -344,7 +335,7 @@ class MtpDatabase {
if (cursor.moveToNext()) { if (cursor.moveToNext()) {
return createIdentifier(cursor.getString(0)); return createIdentifier(cursor.getString(0));
} else { } else {
throw new FileNotFoundException("Cannot find a row having ID=" + documentId); throw new FileNotFoundException("Cannot find a row having ID = " + documentId);
} }
} finally { } finally {
cursor.close(); cursor.close();
@@ -438,7 +429,7 @@ class MtpDatabase {
try { try {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (deleteDocumentsAndRootsRecursively( if (deleteDocumentsAndRootsRecursively(
COLUMN_PARENT_DOCUMENT_ID + "=?", COLUMN_PARENT_DOCUMENT_ID + " = ?",
strings(cursor.getString(0)))) { strings(cursor.getString(0)))) {
changed = true; 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(); mDatabase.beginTransaction();
try { try {
int deleted = 0; 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 { private static class OpenHelper extends SQLiteOpenHelper {
public OpenHelper(Context context, int flags) { public OpenHelper(Context context, int flags) {
super(context, super(context,
@@ -497,6 +557,12 @@ class MtpDatabase {
@Override @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 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(); throw new UnsupportedOperationException();
} }
} }

View File

@@ -30,7 +30,7 @@ import java.util.Map;
* Class containing MtpDatabase constants. * Class containing MtpDatabase constants.
*/ */
class MtpDatabaseConstants { class MtpDatabaseConstants {
static final int DATABASE_VERSION = 1; static final int DATABASE_VERSION = 2;
static final String DATABASE_NAME = "database"; static final String DATABASE_NAME = "database";
static final int FLAG_DATABASE_IN_MEMORY = 1; static final int FLAG_DATABASE_IN_MEMORY = 1;
@@ -77,6 +77,12 @@ class MtpDatabaseConstants {
*/ */
static final int ROW_STATE_INVALIDATED = 1; 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. * Mapping mode that uses MTP identifier to find corresponding rows.
*/ */
@@ -113,7 +119,7 @@ class MtpDatabaseConstants {
"CREATE TABLE " + TABLE_DOCUMENTS + " (" + "CREATE TABLE " + TABLE_DOCUMENTS + " (" +
Document.COLUMN_DOCUMENT_ID + Document.COLUMN_DOCUMENT_ID +
" INTEGER PRIMARY KEY AUTOINCREMENT," + " INTEGER PRIMARY KEY AUTOINCREMENT," +
COLUMN_DEVICE_ID + " INTEGER NOT NULL," + COLUMN_DEVICE_ID + " INTEGER," +
COLUMN_STORAGE_ID + " INTEGER," + COLUMN_STORAGE_ID + " INTEGER," +
COLUMN_OBJECT_HANDLE + " INTEGER," + COLUMN_OBJECT_HANDLE + " INTEGER," +
COLUMN_PARENT_DOCUMENT_ID + " INTEGER," + COLUMN_PARENT_DOCUMENT_ID + " INTEGER," +

View File

@@ -95,7 +95,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
assertEquals(0, getInt(cursor, COLUMN_DEVICE_ID)); assertEquals(0, getInt(cursor, COLUMN_DEVICE_ID));
assertEquals(1, getInt(cursor, COLUMN_STORAGE_ID)); assertEquals(1, getInt(cursor, COLUMN_STORAGE_ID));
assertTrue(isNull(cursor, COLUMN_OBJECT_HANDLE)); 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)); assertEquals("Storage", getString(cursor, COLUMN_DISPLAY_NAME));
assertTrue(isNull(cursor, COLUMN_SUMMARY)); assertTrue(isNull(cursor, COLUMN_SUMMARY));
assertTrue(isNull(cursor, COLUMN_LAST_MODIFIED)); assertTrue(isNull(cursor, COLUMN_LAST_MODIFIED));
@@ -103,7 +104,8 @@ public class MtpDatabaseTest extends AndroidTestCase {
assertEquals(0, getInt(cursor, COLUMN_FLAGS)); assertEquals(0, getInt(cursor, COLUMN_FLAGS));
assertEquals(1000, getInt(cursor, COLUMN_SIZE)); assertEquals(1000, getInt(cursor, COLUMN_SIZE));
assertEquals( assertEquals(
MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE, getInt(cursor, COLUMN_DOCUMENT_TYPE)); MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE,
getInt(cursor, COLUMN_DOCUMENT_TYPE));
cursor.close(); cursor.close();
} }
@@ -296,15 +298,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
{ {
final Cursor cursor = mDatabase.queryRootDocuments(columns); final Cursor cursor = mDatabase.queryRootDocuments(columns);
assertEquals(2, cursor.getCount()); assertEquals(0, 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));
cursor.close(); cursor.close();
} }
@@ -314,28 +308,10 @@ public class MtpDatabaseTest extends AndroidTestCase {
new MtpRoot(0, 200, "Storage A", 2000, 0, ""), new MtpRoot(0, 200, "Storage A", 2000, 0, ""),
new MtpRoot(0, 202, "Storage C", 2002, 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"); mDatabase.getMapper().stopAddingDocuments("1");
{ {
// After compeleting mapping, Storage A can be obtained with new storage ID.
final Cursor cursor = mDatabase.queryRootDocuments(columns); final Cursor cursor = mDatabase.queryRootDocuments(columns);
assertEquals(2, cursor.getCount()); assertEquals(2, cursor.getCount());
cursor.moveToNext(); cursor.moveToNext();
@@ -372,24 +348,9 @@ public class MtpDatabaseTest extends AndroidTestCase {
addTestStorage("1"); addTestStorage("1");
{ {
// Don't return objects that lost MTP object handles.
final Cursor cursor = mDatabase.queryChildDocuments(columns, "2"); final Cursor cursor = mDatabase.queryChildDocuments(columns, "2");
assertEquals(3, cursor.getCount()); assertEquals(0, 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));
cursor.close(); cursor.close();
} }
@@ -598,10 +559,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
Root.COLUMN_AVAILABLE_BYTES Root.COLUMN_AVAILABLE_BYTES
}; };
mDatabase.getMapper().startAddingDocuments(null); addTestDevice();
mDatabase.getMapper().putDeviceDocument(new MtpDeviceRecord(
0, "Device", false, new MtpRoot[0], null, null));
mDatabase.getMapper().stopAddingDocuments(null);
mDatabase.getMapper().startAddingDocuments("1"); mDatabase.getMapper().startAddingDocuments("1");
mDatabase.getMapper().putStorageDocuments("1", new MtpRoot[] { mDatabase.getMapper().putStorageDocuments("1", new MtpRoot[] {
@@ -701,10 +659,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
} }
public void testReplaceExistingRoots() throws Exception { public void testReplaceExistingRoots() throws Exception {
mDatabase.getMapper().startAddingDocuments(null); addTestDevice();
mDatabase.getMapper().putDeviceDocument(new MtpDeviceRecord(
0, "Device", true, new MtpRoot[0], null, null));
mDatabase.getMapper().stopAddingDocuments(null);
// The client code should be able to replace existing rows with new information. // The client code should be able to replace existing rows with new information.
// Add one. // Add one.
@@ -802,10 +757,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
public void testQueryRoots() throws Exception { public void testQueryRoots() throws Exception {
// Add device document. // Add device document.
mDatabase.getMapper().startAddingDocuments(null); addTestDevice();
mDatabase.getMapper().putDeviceDocument(new MtpDeviceRecord(
0, "Device", false, new MtpRoot[0], null, null));
mDatabase.getMapper().stopAddingDocuments(null);
// It the device does not have storages, it shows a device root. // 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. // The new document should not be mapped with existing invalidated document.
mDatabase.getMapper().clearMapping(); mDatabase.getMapper().clearMapping();
addTestDevice();
addTestStorage("1");
mDatabase.getMapper().startAddingDocuments("2"); mDatabase.getMapper().startAddingDocuments("2");
mDatabase.putNewDocument( mDatabase.putNewDocument(
0, 0,