Integerate mapping methods for root/child documents into the same
methods.

The integrated methods will be used to add device documents as well as
root/child documents.

BUG=26175081

Change-Id: Ibf474cfbc41df402a2958e9efcdd0061f07f5ced
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java b/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java
index 0d9d60c..15ebe99 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/Mapper.java
@@ -18,6 +18,7 @@
 
 import static com.android.mtp.MtpDatabaseConstants.*;
 
+import android.annotation.Nullable;
 import android.content.ContentValues;
 import android.content.res.Resources;
 import android.database.Cursor;
@@ -36,7 +37,6 @@
 
 import static com.android.mtp.MtpDatabase.strings;
 
-
 /**
  * Mapping operations for MtpDatabase.
  * Also see the comments of {@link MtpDatabase}.
@@ -45,8 +45,9 @@
     private final MtpDatabase mDatabase;
 
     /**
-     * Mapping mode for roots/documents where we start adding child documents.
+     * Mapping mode for a parent. The key is document ID of parent, or null for root documents.
      * Methods operate the state needs to be synchronized.
+     * TODO: Replace this with unboxing int map.
      */
     private final Map<String, Integer> mMappingMode = new HashMap<>();
 
@@ -55,32 +56,6 @@
     }
 
     /**
-     * Invokes {@link #startAddingDocuments} for root documents.
-     * @param deviceId Device ID.
-     */
-    synchronized void startAddingRootDocuments(int deviceId) {
-        final String mappingStateKey = getRootDocumentsMappingStateKey(deviceId);
-        Preconditions.checkState(!mMappingMode.containsKey(mappingStateKey));
-        mMappingMode.put(
-                mappingStateKey,
-                startAddingDocuments(
-                        SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
-    }
-
-    /**
-     * Invokes {@link #startAddingDocuments} for child of specific documents.
-     * @param parentDocumentId Document ID for parent document.
-     */
-    @VisibleForTesting
-    synchronized void startAddingChildDocuments(String parentDocumentId) {
-        final String mappingStateKey = getChildDocumentsMappingStateKey(parentDocumentId);
-        Preconditions.checkState(!mMappingMode.containsKey(mappingStateKey));
-        mMappingMode.put(
-                mappingStateKey,
-                startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
-    }
-
-    /**
      * Puts root information to database.
      * @param deviceId Device ID
      * @param resources Resources required to localize root name.
@@ -93,9 +68,8 @@
         try {
             final boolean heuristic;
             final String mapColumn;
-            final String key = getRootDocumentsMappingStateKey(deviceId);
-            Preconditions.checkState(mMappingMode.containsKey(key));
-            switch (mMappingMode.get(key)) {
+            Preconditions.checkState(mMappingMode.containsKey(/* no parent for root */ null));
+            switch (mMappingMode.get(/* no parent for root */ null)) {
                 case MAP_BY_MTP_IDENTIFIER:
                     heuristic = false;
                     mapColumn = COLUMN_STORAGE_ID;
@@ -117,8 +91,8 @@
             }
             final boolean changed = putDocuments(
                     valuesList,
-                    SELECTION_ROOT_DOCUMENTS,
-                    Integer.toString(deviceId),
+                    COLUMN_PARENT_DOCUMENT_ID + " IS NULL",
+                    new String[0],
                     heuristic,
                     mapColumn);
             final ContentValues values = new ContentValues();
@@ -156,9 +130,8 @@
     synchronized void putChildDocuments(int deviceId, String parentId, MtpObjectInfo[] documents) {
         final boolean heuristic;
         final String mapColumn;
-        final String key = getChildDocumentsMappingStateKey(parentId);
-        Preconditions.checkState(mMappingMode.containsKey(key));
-        switch (mMappingMode.get(key)) {
+        Preconditions.checkState(mMappingMode.containsKey(parentId));
+        switch (mMappingMode.get(parentId)) {
             case MAP_BY_MTP_IDENTIFIER:
                 heuristic = false;
                 mapColumn = COLUMN_OBJECT_HANDLE;
@@ -177,59 +150,11 @@
                     valuesList[i], deviceId, parentId, documents[i]);
         }
         putDocuments(
-                valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
-    }
-
-    /**
-     * Stops adding root documents.
-     * @param deviceId Device ID.
-     * @return True if new rows are added/removed.
-     */
-    synchronized boolean stopAddingRootDocuments(int deviceId) {
-        final String key = getRootDocumentsMappingStateKey(deviceId);
-        Preconditions.checkState(mMappingMode.containsKey(key));
-        switch (mMappingMode.get(key)) {
-            case MAP_BY_MTP_IDENTIFIER:
-                mMappingMode.remove(key);
-                return stopAddingDocuments(
-                        SELECTION_ROOT_DOCUMENTS,
-                        Integer.toString(deviceId),
-                        COLUMN_STORAGE_ID);
-            case MAP_BY_NAME:
-                mMappingMode.remove(key);
-                return stopAddingDocuments(
-                        SELECTION_ROOT_DOCUMENTS,
-                        Integer.toString(deviceId),
-                        Document.COLUMN_DISPLAY_NAME);
-            default:
-                throw new Error("Unexpected mapping state.");
-        }
-    }
-
-    /**
-     * Stops adding documents under the parent.
-     * @param parentId Document ID of the parent.
-     */
-    synchronized void stopAddingChildDocuments(String parentId) {
-        final String key = getChildDocumentsMappingStateKey(parentId);
-        Preconditions.checkState(mMappingMode.containsKey(key));
-        switch (mMappingMode.get(key)) {
-            case MAP_BY_MTP_IDENTIFIER:
-                stopAddingDocuments(
-                        SELECTION_CHILD_DOCUMENTS,
-                        parentId,
-                        COLUMN_OBJECT_HANDLE);
-                break;
-            case MAP_BY_NAME:
-                stopAddingDocuments(
-                        SELECTION_CHILD_DOCUMENTS,
-                        parentId,
-                        Document.COLUMN_DISPLAY_NAME);
-                break;
-            default:
-                throw new Error("Unexpected mapping state.");
-        }
-        mMappingMode.remove(key);
+                valuesList,
+                COLUMN_PARENT_DOCUMENT_ID + "=?",
+                strings(parentId),
+                heuristic,
+                mapColumn);
     }
 
     @VisibleForTesting
@@ -257,31 +182,42 @@
      * 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.
+     * @param parentDocumentId Parent document ID or NULL for root documents.
      */
-    private int startAddingDocuments(String selection, String arg) {
+    void startAddingDocuments(@Nullable String parentDocumentId) {
+        Preconditions.checkState(!mMappingMode.containsKey(parentDocumentId));
+        final String selection;
+        final String[] args;
+        if (parentDocumentId != null) {
+            selection = COLUMN_PARENT_DOCUMENT_ID + " = ?";
+            args = strings(parentDocumentId);
+        } else {
+            selection = COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
+            args = new String[0];
+        }
+
         final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
         database.beginTransaction();
         try {
             // Delete all pending rows.
             mDatabase.deleteDocumentsAndRootsRecursively(
-                    selection + " AND " + COLUMN_ROW_STATE + "=?", strings(arg, ROW_STATE_PENDING));
+                    selection + " AND " + COLUMN_ROW_STATE + "=?",
+                    DatabaseUtils.appendSelectionArgs(args, strings(ROW_STATE_PENDING)));
 
             // Set all documents as invalidated.
             final ContentValues values = new ContentValues();
             values.put(COLUMN_ROW_STATE, ROW_STATE_INVALIDATED);
-            database.update(TABLE_DOCUMENTS, values, selection, new String[] { arg });
+            database.update(TABLE_DOCUMENTS, values, selection, args);
 
             // 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",
-                    new String[] { arg }) > 0;
+                    args) > 0;
             database.setTransactionSuccessful();
-            return useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER;
+            mMappingMode.put(
+                    parentDocumentId, useNameForResolving ? MAP_BY_NAME : MAP_BY_MTP_IDENTIFIER);
         } finally {
             database.endTransaction();
         }
@@ -292,19 +228,19 @@
      * 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'
+     * {@link #stopAddingDocuments(String, String[], String)} turns the pending rows into 'valid'
      * rows. If the methods adds rows to database, it updates valueList with correct document ID.
      *
      * @param valuesList Values for documents to be stored in the database.
      * @param selection SQL where closure to select rows that shares the same parent.
-     * @param arg Argument for selection SQL.
+     * @param args Argument for selection SQL.
      * @param heuristic Whether the mapping mode is heuristic.
      * @return Whether the method adds new rows.
      */
     private boolean putDocuments(
             ContentValues[] valuesList,
             String selection,
-            String arg,
+            String[] args,
             boolean heuristic,
             String mappingKey) {
         final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
@@ -318,7 +254,9 @@
                         selection + " AND " +
                         COLUMN_ROW_STATE + "=? AND " +
                         mappingKey + "=?",
-                        strings(arg, ROW_STATE_INVALIDATED, values.getAsString(mappingKey)),
+                        DatabaseUtils.appendSelectionArgs(
+                                args,
+                                strings(ROW_STATE_INVALIDATED, values.getAsString(mappingKey))),
                         null,
                         null,
                         null,
@@ -362,12 +300,32 @@
      * 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.
+     * @param parentId Parent document ID or null for root documents.
      * @return Whether the methods adds or removed visible rows.
      */
-    private boolean stopAddingDocuments(String selection, String arg, String groupKey) {
+    boolean stopAddingDocuments(@Nullable String parentId) {
+        Preconditions.checkState(mMappingMode.containsKey(parentId));
+        final String selection;
+        final String[] args;
+        if (parentId != null) {
+            selection = COLUMN_PARENT_DOCUMENT_ID + "=?";
+            args = strings(parentId);
+        } else {
+            selection = COLUMN_PARENT_DOCUMENT_ID + " IS NULL";
+            args = new String[0];
+        }
+        final String groupKey;
+        switch (mMappingMode.get(parentId)) {
+            case MAP_BY_MTP_IDENTIFIER:
+                groupKey = parentId != null ? COLUMN_OBJECT_HANDLE : COLUMN_STORAGE_ID;
+                break;
+            case MAP_BY_NAME:
+                groupKey = Document.COLUMN_DISPLAY_NAME;
+                break;
+            default:
+                throw new Error("Unexpected mapping state.");
+        }
+        mMappingMode.remove(parentId);
         final SQLiteDatabase database = mDatabase.getSQLiteDatabase();
         database.beginTransaction();
         try {
@@ -390,7 +348,7 @@
                             "group_concat(" + pendingIdQuery + ")"
                     },
                     selection,
-                    strings(arg),
+                    args,
                     groupKey,
                     "count(" + invalidatedIdQuery + ") = 1 AND count(" + pendingIdQuery + ") = 1",
                     null);
@@ -439,7 +397,7 @@
             // Delete all invalidated rows that cannot be mapped.
             if (mDatabase.deleteDocumentsAndRootsRecursively(
                     COLUMN_ROW_STATE + " = ? AND " + selection,
-                    strings(ROW_STATE_INVALIDATED, arg))) {
+                    DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_INVALIDATED), args))) {
                 changed = true;
             }
 
@@ -452,7 +410,7 @@
                     TABLE_DOCUMENTS,
                     values,
                     COLUMN_ROW_STATE + " = ? AND " + selection,
-                    strings(ROW_STATE_PENDING, arg)) != 0) {
+                    DatabaseUtils.appendSelectionArgs(strings(ROW_STATE_PENDING), args)) != 0) {
                 changed = true;
             }
             database.setTransactionSuccessful();
@@ -494,20 +452,4 @@
         return "CASE WHEN " + COLUMN_ROW_STATE + " = " + Integer.toString(state) +
                 " THEN " + a + " ELSE NULL END";
     }
-
-    /**
-     * @param deviceId Device ID.
-     * @return Key for {@link #mMappingMode}.
-     */
-    private static String getRootDocumentsMappingStateKey(int deviceId) {
-        return "RootDocuments/" + deviceId;
-    }
-
-    /**
-     * @param parentDocumentId Document ID for the parent document.
-     * @return Key for {@link #mMappingMode}.
-     */
-    private static String getChildDocumentsMappingStateKey(String parentDocumentId) {
-        return "ChildDocuments/" + parentDocumentId;
-    }
 }