Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 49f920fb authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Add new methods to MtpDatabase.

 * deleteDocument
 * getParentId
 * queryDocument
 * putNewDocument

BUG=25756881

Change-Id: Ie223b37e04586b3e2b0448d09e14562fedbb652a
parent e1d57710
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -78,6 +78,12 @@ class DocumentLoader {
            if (parentHandle == CursorHelper.DUMMY_HANDLE_FOR_ROOT) {
                parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
            }
            // TODO: Handle nit race around here.
            // 1. getObjectHandles.
            // 2. putNewDocument.
            // 3. startAddingChildDocuemnts.
            // 4. stopAddingChildDocuments - It removes the new document added at the step 2,
            //     because it is not updated between start/stopAddingChildDocuments.
            task = new LoaderTask(mDatabase, parent, mMtpManager.getObjectHandles(
                    parent.mDeviceId, parent.mStorageId, parentHandle));
            task.fillDocuments(loadDocuments(
+43 −1
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.provider.DocumentsContract.Root;

import com.android.internal.annotations.VisibleForTesting;

import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;

@@ -133,6 +134,14 @@ class MtpDatabase {
        return mDatabase.queryChildDocuments(newColumnNames, parentDocumentId);
    }

    /**
     * {@link MtpDatabaseInternal#queryDocument}
     */
    @VisibleForTesting
    Cursor queryDocument(String documentId, String[] projection) {
        return mDatabase.queryDocument(documentId, projection);
    }

    Identifier createIdentifier(String parentDocumentId) {
        return mDatabase.createIdentifier(parentDocumentId);
    }
@@ -144,6 +153,35 @@ class MtpDatabase {
        mDatabase.removeDeviceRows(deviceId);
    }

    /**
     * {@link MtpDatabaseInternal#getParentId}
     * @throws FileNotFoundException
     */
    @VisibleForTesting
    String getParentId(String documentId) throws FileNotFoundException {
        return mDatabase.getParentId(documentId);
    }

    /**
     * {@link MtpDatabaseInternal#deleteDocument}
     */
    @VisibleForTesting
    void deleteDocument(String documentId) {
        mDatabase.deleteDocument(documentId);
    }

    /**
     * {@link MtpDatabaseInternal#putNewDocument}
     * @throws FileNotFoundException
     */
    @VisibleForTesting
    String putNewDocument(int deviceId, String parentDocumentId, MtpObjectInfo info)
            throws FileNotFoundException {
        final ContentValues values = new ContentValues();
        getChildDocumentValues(values, deviceId, parentDocumentId, info);
        return mDatabase.putNewDocument(parentDocumentId, values);
    }

    /**
     * Invokes {@link MtpDatabaseInternal#startAddingDocuments} for root documents.
     * @param deviceId Device ID.
@@ -186,7 +224,11 @@ class MtpDatabase {
        try {
            final boolean heuristic;
            final String mapColumn;
            switch (mMappingMode.get(getRootDocumentsMappingStateKey(deviceId))) {
            final String key = getRootDocumentsMappingStateKey(deviceId);
            if (!mMappingMode.containsKey(key)) {
                throw new IllegalStateException("startAddingRootDocuments has not been called.");
            }
            switch (mMappingMode.get(key)) {
                case MAP_BY_MTP_IDENTIFIER:
                    heuristic = false;
                    mapColumn = COLUMN_STORAGE_ID;
+114 −5
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.database.sqlite.SQLiteQueryBuilder;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;

import java.io.FileNotFoundException;
import java.util.Objects;

/**
@@ -117,14 +118,87 @@ class MtpDatabaseInternal {
                null);
    }

    /**
     * Queries a single document.
     * @param documentId
     * @param projection
     * @return Database cursor.
     */
    public Cursor queryDocument(String documentId, String[] projection) {
        return mDatabase.query(
                TABLE_DOCUMENTS,
                projection,
                SELECTION_DOCUMENT_ID,
                strings(documentId),
                null,
                null,
                null,
                "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));
    }

    /**
     * Obtains parent document ID.
     * @param documentId
     * @return parent document ID.
     * @throws FileNotFoundException
     */
    String getParentId(String documentId) throws FileNotFoundException {
        final Cursor cursor = mDatabase.query(
                TABLE_DOCUMENTS,
                strings(COLUMN_PARENT_DOCUMENT_ID),
                SELECTION_DOCUMENT_ID,
                strings(documentId),
                null,
                null,
                null,
                "1");
        try {
            if (cursor.moveToNext()) {
                return cursor.getString(0);
            } else {
                throw new FileNotFoundException("Cannot find a row having ID=" + documentId);
            }
        } finally {
            cursor.close();
        }
    }

    /**
     * Deletes document and its children.
     * @param documentId
     */
    void deleteDocument(String documentId) {
        deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, strings(documentId));
    }

    /**
     * Adds new document under the parent.
     * The method does not affect invalidated and pending documents because we know the document is
     * newly added and never mapped with existing ones.
     * @param parentDocumentId
     * @param values
     * @return Document ID of added document.
     */
    String putNewDocument(String parentDocumentId, ContentValues values) {
        mDatabase.beginTransaction();
        try {
            final long id = mDatabase.insert(TABLE_DOCUMENTS, null, values);
            mDatabase.setTransactionSuccessful();
            return Long.toString(id);
        } finally {
            mDatabase.endTransaction();
        }
    }

    /**
     * Gets identifier from document ID.
     * @param documentId Document ID.
@@ -197,7 +271,7 @@ class MtpDatabaseInternal {
        mDatabase.beginTransaction();
        try {
            // Delete all pending rows.
            deleteDocumentsAndRoots(
            deleteDocumentsAndRootsRecursively(
                    selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));

            // Set all documents as invalidated.
@@ -366,14 +440,14 @@ class MtpDatabaseInternal {
                }

                // Delete 'pending' row.
                deleteDocumentsAndRoots(SELECTION_DOCUMENT_ID, new String[] { pendingId });
                deleteDocumentsAndRootsRecursively(SELECTION_DOCUMENT_ID, new String[] { pendingId });
            }
            mergingCursor.close();

            boolean changed = false;

            // Delete all invalidated rows that cannot be mapped.
            if (deleteDocumentsAndRoots(
            if (deleteDocumentsAndRootsRecursively(
                    COLUMN_ROW_STATE + " = ? AND " + selection,
                    strings(ROW_STATE_INVALIDATED, arg))) {
                changed = true;
@@ -407,7 +481,7 @@ class MtpDatabaseInternal {
    void clearMapping() {
        mDatabase.beginTransaction();
        try {
            deleteDocumentsAndRoots(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
            deleteDocumentsAndRootsRecursively(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
            final ContentValues values = new ContentValues();
            values.putNull(COLUMN_OBJECT_HANDLE);
            values.putNull(COLUMN_STORAGE_ID);
@@ -446,6 +520,39 @@ class MtpDatabaseInternal {
     * @param args Arguments for selection.
     * @return Whether the method deletes rows.
     */
    private boolean deleteDocumentsAndRootsRecursively(String selection, String[] args) {
        mDatabase.beginTransaction();
        try {
            boolean changed = false;
            final Cursor cursor = mDatabase.query(
                    TABLE_DOCUMENTS,
                    strings(Document.COLUMN_DOCUMENT_ID),
                    selection,
                    args,
                    null,
                    null,
                    null);
            try {
                while (cursor.moveToNext()) {
                    if (deleteDocumentsAndRootsRecursively(
                            COLUMN_PARENT_DOCUMENT_ID + "=?",
                            strings(cursor.getString(0)))) {
                        changed = true;
                    }
                }
            } finally {
                cursor.close();
            }
            if (deleteDocumentsAndRoots(selection, args)) {
                changed = true;
            }
            mDatabase.setTransactionSuccessful();
            return changed;
        } finally {
            mDatabase.endTransaction();
        }
    }

    private boolean deleteDocumentsAndRoots(String selection, String[] args) {
        mDatabase.beginTransaction();
        try {
@@ -464,6 +571,8 @@ class MtpDatabaseInternal {
                    args);
            deleted += mDatabase.delete(TABLE_DOCUMENTS, selection, args);
            mDatabase.setTransactionSuccessful();
            // TODO Remove child.
            // TODO Remove mappingState.
            return deleted != 0;
        } finally {
            mDatabase.endTransaction();
@@ -505,7 +614,7 @@ class MtpDatabaseInternal {
     * @param args Values converted into string array.
     * @return String array.
     */
    private static String[] strings(Object... args) {
    static String[] strings(Object... args) {
        final String[] results = new String[args.length];
        for (int i = 0; i < args.length; i++) {
            results[i] = Objects.toString(args[i]);
+145 −9
Original line number Diff line number Diff line
@@ -20,10 +20,17 @@ import android.database.Cursor;
import android.mtp.MtpConstants;
import android.mtp.MtpObjectInfo;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;

import java.io.FileNotFoundException;
import java.util.Set;
import java.util.TreeSet;

import static com.android.mtp.MtpDatabaseInternal.strings;

@SmallTest
public class MtpDatabaseTest extends AndroidTestCase {
    private final String[] COLUMN_NAMES = new String[] {
@@ -692,7 +699,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
        }
    }

    public void _testFailToReplaceExisitingUnmappedRoots() {
    public void testFailToReplaceExisitingUnmappedRoots() {
        // The client code should not be able to replace rows before resolving 'unmapped' rows.
        // Add one.
        mDatabase.startAddingRootDocuments(0);
@@ -700,18 +707,147 @@ public class MtpDatabaseTest extends AndroidTestCase {
                new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
        });
        mDatabase.clearMapping();
        final Cursor oldCursor = mDatabase.queryRoots(strings(Root.COLUMN_ROOT_ID));
        assertEquals(1, oldCursor.getCount());

        // Add one.
        mDatabase.startAddingRootDocuments(0);
        mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
                new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
                new MtpRoot(0, 101, "Device", "Storage B", 1000, 1000, ""),
        });
        // Add one more before resolving unmapped documents.
        try {
        mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
                    new MtpRoot(0, 100, "Device", "Storage B", 1000, 1000, ""),
                new MtpRoot(0, 102, "Device", "Storage B", 1000, 1000, ""),
        });
        mDatabase.stopAddingRootDocuments(0);

        // Because the roots shares the same name, the roots should have new IDs.
        final Cursor newCursor = mDatabase.queryRoots(strings(Root.COLUMN_ROOT_ID));
        assertEquals(2, newCursor.getCount());
        oldCursor.moveToNext();
        newCursor.moveToNext();
        assertFalse(oldCursor.getString(0).equals(newCursor.getString(0)));
        newCursor.moveToNext();
        assertFalse(oldCursor.getString(0).equals(newCursor.getString(0)));

        oldCursor.close();
        newCursor.close();
    }

    public void testQueryDocument() {
        mDatabase.startAddingRootDocuments(0);
        mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
                new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
        });
        mDatabase.stopAddingRootDocuments(0);

        final Cursor cursor = mDatabase.queryDocument("1", strings(Document.COLUMN_DISPLAY_NAME));
        assertEquals(1, cursor.getCount());
        cursor.moveToNext();
        assertEquals("Device Storage A", cursor.getString(0));
        cursor.close();
    }

    public void testGetParentId() throws FileNotFoundException {
        mDatabase.startAddingRootDocuments(0);
        mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
                new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
        });
        mDatabase.stopAddingRootDocuments(0);

        mDatabase.startAddingChildDocuments("1");
        mDatabase.putChildDocuments(
                0,
                "1",
                new MtpObjectInfo[] {
                        createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
                });
        mDatabase.stopAddingChildDocuments("1");

        assertEquals("1", mDatabase.getParentId("2"));
    }

    public void testDeleteDocument() {
        mDatabase.startAddingRootDocuments(0);
        mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
                new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
        });
            fail();
        } catch (Throwable e) {
            assertTrue(e instanceof Error);
        mDatabase.stopAddingRootDocuments(0);

        mDatabase.startAddingChildDocuments("1");
        mDatabase.putChildDocuments(
                0,
                "1",
                new MtpObjectInfo[] {
                        createDocument(200, "dir", MtpConstants.FORMAT_ASSOCIATION, 1024),
                });
        mDatabase.stopAddingChildDocuments("1");

        mDatabase.startAddingChildDocuments("2");
        mDatabase.putChildDocuments(
                0,
                "2",
                new MtpObjectInfo[] {
                        createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024),
                });
        mDatabase.stopAddingChildDocuments("2");

        mDatabase.deleteDocument("2");

        {
            // Do not query deleted documents.
            final Cursor cursor =
                    mDatabase.queryChildDocuments(strings(Document.COLUMN_DOCUMENT_ID), "1");
            assertEquals(0, cursor.getCount());
            cursor.close();
        }

        {
            // Child document should be deleted also.
            final Cursor cursor =
                    mDatabase.queryDocument("3", strings(Document.COLUMN_DOCUMENT_ID));
            assertEquals(0, cursor.getCount());
            cursor.close();
        }
    }

    public void testPutNewDocument() throws FileNotFoundException {
        mDatabase.startAddingRootDocuments(0);
        mDatabase.putRootDocuments(0, resources, new MtpRoot[] {
                new MtpRoot(0, 100, "Device", "Storage A", 0, 0, ""),
        });
        mDatabase.stopAddingRootDocuments(0);

        assertEquals(
                "2",
                mDatabase.putNewDocument(
                        0, "1", createDocument(200, "note.txt", MtpConstants.FORMAT_TEXT, 1024)));

        {
            final Cursor cursor =
                    mDatabase.queryChildDocuments(strings(Document.COLUMN_DOCUMENT_ID), "1");
            assertEquals(1, cursor.getCount());
            cursor.moveToNext();
            assertEquals("2", cursor.getString(0));
            cursor.close();
        }

        // The new document should not be mapped with existing invalidated document.
        mDatabase.clearMapping();
        mDatabase.startAddingChildDocuments("1");
        mDatabase.putNewDocument(
                0,
                "1",
                createDocument(201, "note.txt", MtpConstants.FORMAT_TEXT, 1024));
        mDatabase.stopAddingChildDocuments("1");

        {
            final Cursor cursor =
                    mDatabase.queryChildDocuments(strings(Document.COLUMN_DOCUMENT_ID), "1");
            assertEquals(1, cursor.getCount());
            cursor.moveToNext();
            assertEquals("3", cursor.getString(0));
            cursor.close();
        }
    }
}