Merge "Keep metadata of documents as disconnected status after the device is disconnected."

This commit is contained in:
Daichi Hirono
2016-02-10 16:59:11 +00:00
committed by Android (Google) Code Review
4 changed files with 147 additions and 95 deletions

View File

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

View File

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

View File

@@ -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," +

View File

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