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

Commit 18d70d5b authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Split MtpDatabase class into three classes.

BUG=25162822

Change-Id: Id64b6d9fa11a3214bb855ea8e1344af45fb415ff
parent cfcb0c00
Loading
Loading
Loading
Loading
+20 −462
Original line number Diff line number Diff line
@@ -16,14 +16,12 @@

package com.android.mtp;

import static com.android.mtp.MtpDatabaseConstants.*;

import android.content.ContentValues;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.mtp.MtpObjectInfo;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
@@ -32,7 +30,6 @@ import com.android.internal.annotations.VisibleForTesting;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Database for MTP objects.
@@ -73,190 +70,27 @@ import java.util.Objects;
 */
@VisibleForTesting
class MtpDatabase {
    private static final int VERSION = 1;
    private static final String NAME = "mtp";

    /**
     * Table representing documents including root documents.
     */
    private static final String TABLE_DOCUMENTS = "Documents";

    /**
     * Table containing additional information only available for root documents.
     * The table uses same primary keys with corresponding documents.
     */
    private static final String TABLE_ROOT_EXTRA = "RootExtra";

    /**
     * View to join Documents and RootExtra tables to provide roots information.
     */
    private static final String VIEW_ROOTS = "Roots";

    static final String COLUMN_DEVICE_ID = "device_id";
    static final String COLUMN_STORAGE_ID = "storage_id";
    static final String COLUMN_OBJECT_HANDLE = "object_handle";
    static final String COLUMN_PARENT_DOCUMENT_ID = "parent_document_id";
    static final String COLUMN_ROW_STATE = "row_state";

    /**
     * The state represents that the row has a valid object handle.
     */
    static final int ROW_STATE_VALID = 0;

    /**
     * The state represents that the rows added at the previous cycle and need to be updated with
     * fresh values.
     * The row may not have valid object handle. External application can still fetch the documents.
     * If the external application tries to fetch object handle, the provider resolves pending
     * documents with invalidated documents ahead.
     */
    static final int ROW_STATE_INVALIDATED = 1;

    /**
     * The state represents the raw has a valid object handle but it may be going to be mapped with
     * another rows invalidated. After fetching all documents under the parent, the database tries
     * to map the pending documents and the invalidated documents in order to keep old document ID
     * alive.
     */
    static final int ROW_STATE_PENDING = 2;

    /**
     * Mapping mode that uses MTP identifier to find corresponding rows.
     */
    static final int MAP_BY_MTP_IDENTIFIER = 0;

    /**
     * Mapping mode that uses name to find corresponding rows.
     */
    static final int MAP_BY_NAME = 1;

    private static final String SELECTION_DOCUMENT_ID = Document.COLUMN_DOCUMENT_ID + " = ?";
    private static final String SELECTION_ROOT_ID = Root.COLUMN_ROOT_ID + " = ?";
    private static final String SELECTION_ROOT_DOCUMENTS =
            COLUMN_DEVICE_ID + " = ? AND " + COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
    private static final String SELECTION_CHILD_DOCUMENTS = COLUMN_PARENT_DOCUMENT_ID + " = ?";

    static class ParentNotFoundException extends Exception {}

    private static class OpenHelper extends SQLiteOpenHelper {
        private static final String QUERY_CREATE_DOCUMENTS =
                "CREATE TABLE " + TABLE_DOCUMENTS + " (" +
                Document.COLUMN_DOCUMENT_ID +
                    " INTEGER PRIMARY KEY AUTOINCREMENT," +
                COLUMN_DEVICE_ID + " INTEGER NOT NULL," +
                COLUMN_STORAGE_ID + " INTEGER," +
                COLUMN_OBJECT_HANDLE + " INTEGER," +
                COLUMN_PARENT_DOCUMENT_ID + " INTEGER," +
                COLUMN_ROW_STATE + " INTEGER NOT NULL," +
                Document.COLUMN_MIME_TYPE + " TEXT," +
                Document.COLUMN_DISPLAY_NAME + " TEXT NOT NULL," +
                Document.COLUMN_SUMMARY + " TEXT," +
                Document.COLUMN_LAST_MODIFIED + " INTEGER," +
                Document.COLUMN_ICON + " INTEGER," +
                Document.COLUMN_FLAGS + " INTEGER NOT NULL," +
                Document.COLUMN_SIZE + " INTEGER NOT NULL);";

        private static final String QUERY_CREATE_ROOT_EXTRA =
                "CREATE TABLE " + TABLE_ROOT_EXTRA + " (" +
                Root.COLUMN_ROOT_ID + " INTEGER PRIMARY KEY," +
                Root.COLUMN_FLAGS + " INTEGER NOT NULL," +
                Root.COLUMN_AVAILABLE_BYTES + " INTEGER NOT NULL," +
                Root.COLUMN_CAPACITY_BYTES + " INTEGER NOT NULL," +
                Root.COLUMN_MIME_TYPES + " TEXT NOT NULL);";

        /**
         * Creates a view to join Documents table and RootExtra table on their primary keys to
         * provide DocumentContract.Root equivalent information.
         */
        private static final String QUERY_CREATE_VIEW_ROOTS =
                "CREATE VIEW " + VIEW_ROOTS + " AS SELECT " +
                        TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
                                Root.COLUMN_ROOT_ID + "," +
                        TABLE_ROOT_EXTRA + "." + Root.COLUMN_FLAGS + "," +
                        TABLE_DOCUMENTS + "." + Document.COLUMN_ICON + " AS " +
                                Root.COLUMN_ICON + "," +
                        TABLE_DOCUMENTS + "." + Document.COLUMN_DISPLAY_NAME + " AS " +
                                Root.COLUMN_TITLE + "," +
                        TABLE_DOCUMENTS + "." + Document.COLUMN_SUMMARY + " AS " +
                                Root.COLUMN_SUMMARY + "," +
                        TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
                        Root.COLUMN_DOCUMENT_ID + "," +
                        TABLE_ROOT_EXTRA + "." + Root.COLUMN_AVAILABLE_BYTES + "," +
                        TABLE_ROOT_EXTRA + "." + Root.COLUMN_CAPACITY_BYTES + "," +
                        TABLE_ROOT_EXTRA + "." + Root.COLUMN_MIME_TYPES + "," +
                        TABLE_DOCUMENTS + "." + COLUMN_ROW_STATE +
                " FROM " + TABLE_DOCUMENTS + " INNER JOIN " + TABLE_ROOT_EXTRA +
                " ON " +
                        COLUMN_PARENT_DOCUMENT_ID + " IS NULL AND " +
                        TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID +
                        "=" +
                        TABLE_ROOT_EXTRA + "." + Root.COLUMN_ROOT_ID;

        public OpenHelper(Context context) {
            super(context, NAME, null, VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(QUERY_CREATE_DOCUMENTS);
            db.execSQL(QUERY_CREATE_ROOT_EXTRA);
            db.execSQL(QUERY_CREATE_VIEW_ROOTS);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            throw new UnsupportedOperationException();
        }
    }

    private final MtpDatabaseInternal mDatabase;
    private final Map<String, Integer> mMappingMode = new HashMap<>();
    private final SQLiteDatabase mDatabase;

    @VisibleForTesting
    MtpDatabase(Context context) {
        final OpenHelper helper = new OpenHelper(context);
        mDatabase = helper.getWritableDatabase();
    }

    @VisibleForTesting
    static void deleteDatabase(Context context) {
        SQLiteDatabase.deleteDatabase(context.getDatabasePath(NAME));
        mDatabase = new MtpDatabaseInternal(context);
    }

    @VisibleForTesting
    Cursor queryRoots(String[] columnNames) {
        return mDatabase.query(
                VIEW_ROOTS,
                columnNames,
                COLUMN_ROW_STATE + " IN (?, ?)",
                strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
                null,
                null,
                null);
        return mDatabase.queryRoots(columnNames);
    }

    @VisibleForTesting
    Cursor queryRootDocuments(String[] columnNames) {
        return mDatabase.query(
                TABLE_DOCUMENTS,
                columnNames,
                COLUMN_ROW_STATE + " IN (?, ?)",
                strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED),
                null,
                null,
                null);
        return mDatabase.queryRootDocuments(columnNames);
    }

    @VisibleForTesting
    Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
        return mDatabase.query(
                TABLE_DOCUMENTS,
                columnNames,
                COLUMN_ROW_STATE + " IN (?, ?) AND " + COLUMN_PARENT_DOCUMENT_ID + " = ?",
                strings(ROW_STATE_VALID, ROW_STATE_INVALIDATED, parentDocumentId),
                null,
                null,
                null);
        return mDatabase.queryChildDocuments(columnNames, parentDocumentId);
    }

    @VisibleForTesting
@@ -267,7 +101,8 @@ class MtpDatabase {
        }
        mMappingMode.put(
                mappingStateKey,
                startAddingDocuments(SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
                mDatabase.startAddingDocuments(
                        SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
    }

    @VisibleForTesting
@@ -278,43 +113,7 @@ class MtpDatabase {
        }
        mMappingMode.put(
                mappingStateKey,
                startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
    }

    /**
     * Starts adding new documents.
     * The methods decides mapping mode depends on if all documents under the given parent have MTP
     * identifier or not. If all the documents have MTP identifier, it uses the identifier to find
     * a corresponding existing row. Otherwise it does heuristic.
     *
     * @param selection Query matches valid documents.
     * @param arg Argument for selection.
     * @return Mapping mode.
     */
    @VisibleForTesting
    private int startAddingDocuments(String selection, String arg) {
        mDatabase.beginTransaction();
        try {
            // Delete all pending rows.
            deleteDocumentsAndRoots(
                    selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));

            // Set all documents as invalidated.
            final ContentValues values = new ContentValues();
            values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
            mDatabase.update(TABLE_DOCUMENTS, values, selection, new String[] { arg });

            // If we have rows that does not have MTP identifier, do heuristic mapping by name.
            final boolean useNameForResolving = DatabaseUtils.queryNumEntries(
                    mDatabase,
                    TABLE_DOCUMENTS,
                    selection + " AND " + COLUMN_STORAGE_ID + " IS NULL",
                    new String[] { arg }) > 0;
            mDatabase.setTransactionSuccessful();
            return useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER;
        } finally {
            mDatabase.endTransaction();
        }
                mDatabase.startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
    }

    @VisibleForTesting
@@ -343,7 +142,7 @@ class MtpDatabase {
                default:
                    throw new Error("Unexpected map mode.");
            }
            final long[] documentIds = putDocuments(
            final long[] documentIds = mDatabase.putDocuments(
                    valuesList,
                    SELECTION_ROOT_DOCUMENTS,
                    Integer.toString(deviceId),
@@ -359,7 +158,7 @@ class MtpDatabase {
                values.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
                values.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
                values.put(Root.COLUMN_MIME_TYPES, "");
                mDatabase.replace(TABLE_ROOT_EXTRA, null, values);
                mDatabase.putRootExtra(values);
            }
            mDatabase.setTransactionSuccessful();
        } finally {
@@ -388,30 +187,14 @@ class MtpDatabase {
            default:
                throw new Error("Unexpected map mode.");
        }
        putDocuments(valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
        mDatabase.putDocuments(
                valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
    }

    /**
     * Clears MTP related identifier.
     * It clears MTP's object handle and storage ID that are not stable over MTP sessions and mark
     * the all documents as 'invalidated'. It also remove 'pending' rows as adding is cancelled
     * now.
     */
    @VisibleForTesting
    void clearMapping() {
        mDatabase.beginTransaction();
        try {
            deleteDocumentsAndRoots(COLUMN_ROW_STATE + " = ?", strings(ROW_STATE_PENDING));
            final ContentValues values = new ContentValues();
            values.putNull(COLUMN_OBJECT_HANDLE);
            values.putNull(COLUMN_STORAGE_ID);
            values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
            mDatabase.update(TABLE_DOCUMENTS, values, null, null);
            mDatabase.setTransactionSuccessful();
        mDatabase.clearMapping();
        mMappingMode.clear();
        } finally {
            mDatabase.endTransaction();
        }
    }

    @VisibleForTesting
@@ -419,13 +202,13 @@ class MtpDatabase {
        final String mappingModeKey = getRootDocumentsMappingStateKey(deviceId);
        switch (mMappingMode.get(mappingModeKey)) {
            case MAP_BY_MTP_IDENTIFIER:
                stopAddingDocuments(
                mDatabase.stopAddingDocuments(
                        SELECTION_ROOT_DOCUMENTS,
                        Integer.toString(deviceId),
                        COLUMN_STORAGE_ID);
                break;
            case MAP_BY_NAME:
                stopAddingDocuments(
                mDatabase.stopAddingDocuments(
                        SELECTION_ROOT_DOCUMENTS,
                        Integer.toString(deviceId),
                        Document.COLUMN_DISPLAY_NAME);
@@ -441,13 +224,13 @@ class MtpDatabase {
        final String mappingModeKey = getChildDocumentsMappingStateKey(parentId);
        switch (mMappingMode.get(mappingModeKey)) {
            case MAP_BY_MTP_IDENTIFIER:
                stopAddingDocuments(
                mDatabase.stopAddingDocuments(
                        SELECTION_CHILD_DOCUMENTS,
                        parentId,
                        COLUMN_OBJECT_HANDLE);
                break;
            case MAP_BY_NAME:
                stopAddingDocuments(
                mDatabase.stopAddingDocuments(
                        SELECTION_CHILD_DOCUMENTS,
                        parentId,
                        Document.COLUMN_DISPLAY_NAME);
@@ -458,161 +241,6 @@ class MtpDatabase {
        mMappingMode.remove(mappingModeKey);
    }

    /**
     * Puts the documents into the database.
     * If the mapping mode is not heuristic, it just adds the rows to the database or updates the
     * existing rows with the new values. If the mapping mode is heuristic, it adds some new rows as
     * 'pending' state when that rows may be corresponding to existing 'invalidated' rows. Then
     * {@link #stopAddingDocuments(String, String, String)} turns the pending rows into 'valid'
     * rows.
     *
     * @param valuesList Values that are stored in the database.
     * @param selection SQL where closure to select rows that shares the same parent.
     * @param arg Argument for selection SQL.
     * @param heuristic Whether the mapping mode is heuristic.
     * @return List of Document ID inserted to the table.
     */
    private long[] putDocuments(
            ContentValues[] valuesList,
            String selection,
            String arg,
            boolean heuristic,
            String mappingKey) {
        mDatabase.beginTransaction();
        try {
            final long[] documentIds = new long[valuesList.length];
            int i = 0;
            for (final ContentValues values : valuesList) {
                final Cursor candidateCursor = mDatabase.query(
                        TABLE_DOCUMENTS,
                        strings(Document.COLUMN_DOCUMENT_ID),
                        selection + " AND " +
                        COLUMN_ROW_STATE + "=? AND " +
                        mappingKey + "=?",
                        strings(arg, ROW_STATE_INVALIDATED, values.getAsString(mappingKey)),
                        null,
                        null,
                        null,
                        "1");
                final long rowId;
                if (candidateCursor.getCount() == 0) {
                    rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
                } else if (!heuristic) {
                    candidateCursor.moveToNext();
                    final String documentId = candidateCursor.getString(0);
                    rowId = mDatabase.update(
                            TABLE_DOCUMENTS, values, SELECTION_DOCUMENT_ID, strings(documentId));
                } else {
                    values.put(COLUMN_ROW_STATE, ROW_STATE_PENDING);
                    rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
                }
                // Document ID is a primary integer key of the table. So the returned row
                // IDs should be same with the document ID.
                documentIds[i++] = rowId;
                candidateCursor.close();
            }

            mDatabase.setTransactionSuccessful();
            return documentIds;
        } finally {
            mDatabase.endTransaction();
        }
    }

    /**
     * Maps 'pending' document and 'invalidated' document that shares the same column of groupKey.
     * If the database does not find corresponding 'invalidated' document, it just removes
     * 'invalidated' document from the database.
     * @param selection Query to select rows for resolving.
     * @param arg Argument for selection SQL.
     * @param groupKey Column name used to find corresponding rows.
     */
    private void stopAddingDocuments(String selection, String arg, String groupKey) {
        mDatabase.beginTransaction();
        try {
            // Get 1-to-1 mapping of invalidated document and pending document.
            final String invalidatedIdQuery = createStateFilter(
                    ROW_STATE_INVALIDATED, Document.COLUMN_DOCUMENT_ID);
            final String pendingIdQuery = createStateFilter(
                    ROW_STATE_PENDING, Document.COLUMN_DOCUMENT_ID);
            // SQL should be like:
            // SELECT group_concat(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END),
            //        group_concat(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END)
            // WHERE device_id = ? AND parent_document_id IS NULL
            // GROUP BY display_name
            // HAVING count(CASE WHEN raw_state = 1 THEN document_id ELSE NULL END) = 1 AND
            //        count(CASE WHEN raw_state = 2 THEN document_id ELSE NULL END) = 1
            final Cursor mergingCursor = mDatabase.query(
                    TABLE_DOCUMENTS,
                    new String[] {
                            "group_concat(" + invalidatedIdQuery + ")",
                            "group_concat(" + pendingIdQuery + ")"
                    },
                    selection,
                    strings(arg),
                    groupKey,
                    "count(" + invalidatedIdQuery + ") = 1 AND count(" + pendingIdQuery + ") = 1",
                    null);

            final ContentValues values = new ContentValues();
            while (mergingCursor.moveToNext()) {
                final String invalidatedId = mergingCursor.getString(0);
                final String pendingId = mergingCursor.getString(1);

                // Obtain the new values including the latest object handle from mapping row.
                getFirstRow(
                        TABLE_DOCUMENTS,
                        SELECTION_DOCUMENT_ID,
                        new String[] { pendingId },
                        values);
                values.remove(Document.COLUMN_DOCUMENT_ID);
                values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
                mDatabase.update(
                        TABLE_DOCUMENTS,
                        values,
                        SELECTION_DOCUMENT_ID,
                        new String[] { invalidatedId });

                getFirstRow(
                        TABLE_ROOT_EXTRA,
                        SELECTION_ROOT_ID,
                        new String[] { pendingId },
                        values);
                if (values.size() > 0) {
                    values.remove(Root.COLUMN_ROOT_ID);
                    mDatabase.update(
                            TABLE_ROOT_EXTRA,
                            values,
                            SELECTION_ROOT_ID,
                            new String[] { invalidatedId });
                }

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

            // Delete all invalidated rows that cannot be mapped.
            deleteDocumentsAndRoots(
                    COLUMN_ROW_STATE + " = ? AND " + selection,
                    strings(ROW_STATE_INVALIDATED, arg));

            // The database cannot find old document ID for the pending rows.
            // Turn the all pending rows into valid state, which means the rows become to be
            // valid with new document ID.
            values.clear();
            values.put(COLUMN_ROW_STATE, ROW_STATE_VALID);
            mDatabase.update(
                    TABLE_DOCUMENTS,
                    values,
                    COLUMN_ROW_STATE + " = ? AND " + selection,
                    strings(ROW_STATE_PENDING, arg));
            mDatabase.setTransactionSuccessful();
        } finally {
            mDatabase.endTransaction();
        }
    }

    /**
     * Gets {@link ContentValues} for the given root.
     * @param values {@link ContentValues} that receives values.
@@ -675,51 +303,6 @@ class MtpDatabase {
        values.put(Document.COLUMN_SIZE, info.getCompressedSize());
    }

    /**
     * Obtains values of the first row for the query.
     * @param values ContentValues that the values are stored to.
     * @param table Target table.
     * @param selection Query to select rows.
     * @param args Argument for query.
     */
    private void getFirstRow(String table, String selection, String[] args, ContentValues values) {
        values.clear();
        final Cursor cursor = mDatabase.query(table, null, selection, args, null, null, null, "1");
        if (cursor.getCount() == 0) {
            return;
        }
        cursor.moveToNext();
        DatabaseUtils.cursorRowToContentValues(cursor, values);
        cursor.close();
    }

    /**
     * Deletes a document, and its root information if the document is a root document.
     * @param selection Query to select documents.
     * @param args Arguments for selection.
     */
    private void deleteDocumentsAndRoots(String selection, String[] args) {
        mDatabase.beginTransaction();
        try {
            mDatabase.delete(
                    TABLE_ROOT_EXTRA,
                    Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString(
                            false,
                            TABLE_DOCUMENTS,
                            new String[] { Document.COLUMN_DOCUMENT_ID },
                            selection,
                            null,
                            null,
                            null,
                            null) + ")",
                    args);
            mDatabase.delete(TABLE_DOCUMENTS, selection, args);
            mDatabase.setTransactionSuccessful();
        } finally {
            mDatabase.endTransaction();
        }
    }

    private String getRootDocumentsMappingStateKey(int deviceId) {
        return "RootDocuments/" + deviceId;
    }
@@ -727,29 +310,4 @@ class MtpDatabase {
    private String getChildDocumentsMappingStateKey(String parentDocumentId) {
        return "ChildDocuments/" + parentDocumentId;
    }

    /**
     * Converts values into string array.
     * @param args Values converted into string array.
     * @return String array.
     */
    private 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]);
        }
        return results;
    }

    /**
     * Gets SQL expression that represents the given value or NULL depends on the row state.
     * @param state Expected row state.
     * @param a SQL value.
     * @return Expression that represents a if the row state is expected one, and represents NULL
     *     otherwise.
     */
    private static String createStateFilter(int state, String a) {
        return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
                " THEN " + a + " ELSE NULL END";
    }
}
+141 −0

File added.

Preview size limit exceeded, changes collapsed.

+391 −0

File added.

Preview size limit exceeded, changes collapsed.

+10 −16

File changed.

Preview size limit exceeded, changes collapsed.