OSDN Git Service

Start to use MtpDatabase in RootScanner.
authorDaichi Hirono <hirono@google.com>
Fri, 13 Nov 2015 06:42:28 +0000 (15:42 +0900)
committerDaichi Hirono <hirono@google.com>
Mon, 16 Nov 2015 08:49:20 +0000 (17:49 +0900)
Change-Id: Id99cb61ad8680529b5ee502ca5bb2b3cdd143235

packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseInternal.java
packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDatabaseTest.java
packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java

index e3be534..3151ccb 100644 (file)
@@ -71,6 +71,11 @@ import java.util.Map;
 @VisibleForTesting
 class MtpDatabase {
     private final MtpDatabaseInternal mDatabase;
+
+    /**
+     * Mapping mode for roots/documents where we start adding child documents.
+     * Methods operate the state needs to be synchronized.
+     */
     private final Map<String, Integer> mMappingMode = new HashMap<>();
 
     @VisibleForTesting
@@ -78,23 +83,49 @@ class MtpDatabase {
         mDatabase = new MtpDatabaseInternal(context);
     }
 
+    /**
+     * Closes the database.
+     */
     @VisibleForTesting
+    void close() {
+        mDatabase.close();
+    }
+
+    /**
+     * {@link MtpDatabaseInternal#queryRoots}
+     */
     Cursor queryRoots(String[] columnNames) {
         return mDatabase.queryRoots(columnNames);
     }
 
+    /**
+     * {@link MtpDatabaseInternal#queryRootDocuments}
+     */
     @VisibleForTesting
     Cursor queryRootDocuments(String[] columnNames) {
         return mDatabase.queryRootDocuments(columnNames);
     }
 
+    /**
+     * {@link MtpDatabaseInternal#queryChildDocuments}
+     */
     @VisibleForTesting
     Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
         return mDatabase.queryChildDocuments(columnNames, parentDocumentId);
     }
 
-    @VisibleForTesting
-    void startAddingRootDocuments(int deviceId) {
+    /**
+     * {@link MtpDatabaseInternal#removeDeviceRows}
+     */
+    void removeDeviceRows(int deviceId) {
+        mDatabase.removeDeviceRows(deviceId);
+    }
+
+    /**
+     * Invokes {@link MtpDatabaseInternal#startAddingDocuments} for root documents.
+     * @param deviceId Device ID.
+     */
+    synchronized void startAddingRootDocuments(int deviceId) {
         final String mappingStateKey = getRootDocumentsMappingStateKey(deviceId);
         if (mMappingMode.containsKey(mappingStateKey)) {
             throw new Error("Mapping for the root has already started.");
@@ -105,8 +136,12 @@ class MtpDatabase {
                         SELECTION_ROOT_DOCUMENTS, Integer.toString(deviceId)));
     }
 
+    /**
+     * Invokes {@link MtpDatabaseInternal#startAddingDocuments} for child of specific documents.
+     * @param parentDocumentId Document ID for parent document.
+     */
     @VisibleForTesting
-    void startAddingChildDocuments(String parentDocumentId) {
+    synchronized void startAddingChildDocuments(String parentDocumentId) {
         final String mappingStateKey = getChildDocumentsMappingStateKey(parentDocumentId);
         if (mMappingMode.containsKey(mappingStateKey)) {
             throw new Error("Mapping for the root has already started.");
@@ -116,20 +151,18 @@ class MtpDatabase {
                 mDatabase.startAddingDocuments(SELECTION_CHILD_DOCUMENTS, parentDocumentId));
     }
 
-    @VisibleForTesting
-    void putRootDocuments(int deviceId, Resources resources, MtpRoot[] roots) {
+    /**
+     * Puts root information to database.
+     * @param deviceId Device ID
+     * @param resources Resources required to localize root name.
+     * @param roots List of root information.
+     * @return If roots are added or removed from the database.
+     */
+    synchronized boolean putRootDocuments(int deviceId, Resources resources, MtpRoot[] roots) {
         mDatabase.beginTransaction();
         try {
-            final ContentValues[] valuesList = new ContentValues[roots.length];
-            for (int i = 0; i < roots.length; i++) {
-                if (roots[i].mDeviceId != deviceId) {
-                    throw new IllegalArgumentException();
-                }
-                valuesList[i] = new ContentValues();
-                getRootDocumentValues(valuesList[i], resources, roots[i]);
-            }
-            boolean heuristic;
-            String mapColumn;
+            final boolean heuristic;
+            final String mapColumn;
             switch (mMappingMode.get(getRootDocumentsMappingStateKey(deviceId))) {
                 case MAP_BY_MTP_IDENTIFIER:
                     heuristic = false;
@@ -142,7 +175,15 @@ class MtpDatabase {
                 default:
                     throw new Error("Unexpected map mode.");
             }
-            final long[] documentIds = mDatabase.putDocuments(
+            final ContentValues[] valuesList = new ContentValues[roots.length];
+            for (int i = 0; i < roots.length; i++) {
+                if (roots[i].mDeviceId != deviceId) {
+                    throw new IllegalArgumentException();
+                }
+                valuesList[i] = new ContentValues();
+                getRootDocumentValues(valuesList[i], resources, roots[i]);
+            }
+            final boolean changed = mDatabase.putDocuments(
                     valuesList,
                     SELECTION_ROOT_DOCUMENTS,
                     Integer.toString(deviceId),
@@ -152,33 +193,38 @@ class MtpDatabase {
             int i = 0;
             for (final MtpRoot root : roots) {
                 // Use the same value for the root ID and the corresponding document ID.
-                values.put(Root.COLUMN_ROOT_ID, documentIds[i++]);
                 values.put(
-                        Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
+                        Root.COLUMN_ROOT_ID,
+                        valuesList[i++].getAsString(Document.COLUMN_DOCUMENT_ID));
+                values.put(
+                        Root.COLUMN_FLAGS,
+                        Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
                 values.put(Root.COLUMN_AVAILABLE_BYTES, root.mFreeSpace);
                 values.put(Root.COLUMN_CAPACITY_BYTES, root.mMaxCapacity);
                 values.put(Root.COLUMN_MIME_TYPES, "");
                 mDatabase.putRootExtra(values);
             }
             mDatabase.setTransactionSuccessful();
+            return changed;
         } finally {
             mDatabase.endTransaction();
         }
     }
 
+    /**
+     * Puts document information to database.
+     * @param deviceId Device ID
+     * @param parentId Parent document ID.
+     * @param documents List of document information.
+     */
     @VisibleForTesting
-    void putChildDocuments(int deviceId, String parentId, MtpObjectInfo[] documents) {
-        final ContentValues[] valuesList = new ContentValues[documents.length];
-        for (int i = 0; i < documents.length; i++) {
-            valuesList[i] = new ContentValues();
-            getChildDocumentValues(valuesList[i], deviceId, parentId, documents[i]);
-        }
-        boolean heuristic;
-        String mapColumn;
+    synchronized void putChildDocuments(int deviceId, String parentId, MtpObjectInfo[] documents) {
+        final boolean heuristic;
+        final String mapColumn;
         switch (mMappingMode.get(getChildDocumentsMappingStateKey(parentId))) {
             case MAP_BY_MTP_IDENTIFIER:
                 heuristic = false;
-                mapColumn = COLUMN_STORAGE_ID;
+                mapColumn = COLUMN_OBJECT_HANDLE;
                 break;
             case MAP_BY_NAME:
                 heuristic = true;
@@ -187,40 +233,55 @@ class MtpDatabase {
             default:
                 throw new Error("Unexpected map mode.");
         }
+        final ContentValues[] valuesList = new ContentValues[documents.length];
+        for (int i = 0; i < documents.length; i++) {
+            valuesList[i] = new ContentValues();
+            getChildDocumentValues(valuesList[i], deviceId, parentId, documents[i]);
+        }
         mDatabase.putDocuments(
                 valuesList, SELECTION_CHILD_DOCUMENTS, parentId, heuristic, mapColumn);
     }
 
+    /**
+     * Clears mapping between MTP identifier and document/root ID.
+     */
     @VisibleForTesting
-    void clearMapping() {
+    synchronized void clearMapping() {
         mDatabase.clearMapping();
         mMappingMode.clear();
     }
 
-    @VisibleForTesting
-    void stopAddingRootDocuments(int deviceId) {
+    /**
+     * Stops adding root documents.
+     * @param deviceId Device ID.
+     * @return True if new rows are added/removed.
+     */
+    synchronized boolean stopAddingRootDocuments(int deviceId) {
         final String mappingModeKey = getRootDocumentsMappingStateKey(deviceId);
         switch (mMappingMode.get(mappingModeKey)) {
             case MAP_BY_MTP_IDENTIFIER:
-                mDatabase.stopAddingDocuments(
+                mMappingMode.remove(mappingModeKey);
+                return mDatabase.stopAddingDocuments(
                         SELECTION_ROOT_DOCUMENTS,
                         Integer.toString(deviceId),
                         COLUMN_STORAGE_ID);
-                break;
             case MAP_BY_NAME:
-                mDatabase.stopAddingDocuments(
+                mMappingMode.remove(mappingModeKey);
+                return mDatabase.stopAddingDocuments(
                         SELECTION_ROOT_DOCUMENTS,
                         Integer.toString(deviceId),
                         Document.COLUMN_DISPLAY_NAME);
-                break;
             default:
                 throw new Error("Unexpected mapping state.");
         }
-        mMappingMode.remove(mappingModeKey);
     }
 
+    /**
+     * Stops adding documents under the parent.
+     * @param parentId Document ID of the parent.
+     */
     @VisibleForTesting
-    void stopAddingChildDocuments(String parentId) {
+    synchronized void stopAddingChildDocuments(String parentId) {
         final String mappingModeKey = getChildDocumentsMappingStateKey(parentId);
         switch (mMappingMode.get(mappingModeKey)) {
             case MAP_BY_MTP_IDENTIFIER:
@@ -303,11 +364,19 @@ class MtpDatabase {
         values.put(Document.COLUMN_SIZE, info.getCompressedSize());
     }
 
-    private String getRootDocumentsMappingStateKey(int deviceId) {
+    /**
+     * @param deviceId Device ID.
+     * @return Key for {@link #mMappingMode}.
+     */
+    private static String getRootDocumentsMappingStateKey(int deviceId) {
         return "RootDocuments/" + deviceId;
     }
 
-    private String getChildDocumentsMappingStateKey(String parentDocumentId) {
+    /**
+     * @param parentDocumentId Document ID for the parent document.
+     * @return Key for {@link #mMappingMode}.
+     */
+    private static String getChildDocumentsMappingStateKey(String parentDocumentId) {
         return "ChildDocuments/" + parentDocumentId;
     }
 }
index 5fb16ec..977b12e 100644 (file)
@@ -126,7 +126,11 @@ class MtpDatabaseConstants {
                             Root.COLUMN_TITLE + "," +
                     TABLE_DOCUMENTS + "." + Document.COLUMN_SUMMARY + " AS " +
                             Root.COLUMN_SUMMARY + "," +
+                    // Temporary replace COLUMN_DOCUMENT_ID with old format.
                     TABLE_DOCUMENTS + "." + Document.COLUMN_DOCUMENT_ID + " AS " +
+                    Root.COLUMN_DOCUMENT_ID + "_," +
+                    TABLE_DOCUMENTS + "." + COLUMN_DEVICE_ID + "|| '_' ||" +
+                    TABLE_DOCUMENTS + "." + COLUMN_STORAGE_ID + "||'_0' AS " +
                     Root.COLUMN_DOCUMENT_ID + "," +
                     TABLE_ROOT_EXTRA + "." + Root.COLUMN_AVAILABLE_BYTES + "," +
                     TABLE_ROOT_EXTRA + "." + Root.COLUMN_CAPACITY_BYTES + "," +
index 730012d..7328f05 100644 (file)
@@ -59,6 +59,15 @@ class MtpDatabaseInternal {
         mDatabase = helper.getWritableDatabase();
     }
 
+    void close() {
+        mDatabase.close();
+    }
+
+    /**
+     * Queries roots information.
+     * @param columnNames Column names defined in {@link android.provider.DocumentsContract.Root}.
+     * @return Database cursor.
+     */
     Cursor queryRoots(String[] columnNames) {
         return mDatabase.query(
                 VIEW_ROOTS,
@@ -70,6 +79,12 @@ class MtpDatabaseInternal {
                 null);
     }
 
+    /**
+     * Queries root documents information.
+     * @param columnNames Column names defined in
+     *     {@link android.provider.DocumentsContract.Document}.
+     * @return Database cursor.
+     */
     Cursor queryRootDocuments(String[] columnNames) {
         return mDatabase.query(
                 TABLE_DOCUMENTS,
@@ -81,6 +96,12 @@ class MtpDatabaseInternal {
                 null);
     }
 
+    /**
+     * Queries documents information.
+     * @param columnNames Column names defined in
+     *     {@link android.provider.DocumentsContract.Document}.
+     * @return Database cursor.
+     */
     Cursor queryChildDocuments(String[] columnNames, String parentDocumentId) {
         return mDatabase.query(
                 TABLE_DOCUMENTS,
@@ -93,6 +114,14 @@ class MtpDatabaseInternal {
     }
 
     /**
+     * Remove all rows belong to a device.
+     * @param deviceId Device ID.
+     */
+    void removeDeviceRows(int deviceId) {
+        deleteDocumentsAndRoots(COLUMN_DEVICE_ID + "=?", strings(deviceId));
+    }
+
+    /**
      * 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
@@ -133,24 +162,23 @@ class MtpDatabaseInternal {
      * 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.
+     * rows. If the methods adds rows to database, it updates valueList with correct document ID.
      *
      * @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.
+     * @return Whether the method adds new rows.
      */
-    long[] putDocuments(
+    boolean putDocuments(
             ContentValues[] valuesList,
             String selection,
             String arg,
             boolean heuristic,
             String mappingKey) {
+        boolean added = false;
         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,
@@ -166,6 +194,7 @@ class MtpDatabaseInternal {
                 final long rowId;
                 if (candidateCursor.getCount() == 0) {
                     rowId = mDatabase.insert(TABLE_DOCUMENTS, null, values);
+                    added = true;
                 } else if (!heuristic) {
                     candidateCursor.moveToNext();
                     final String documentId = candidateCursor.getString(0);
@@ -177,17 +206,21 @@ class MtpDatabaseInternal {
                 }
                 // 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;
+                values.put(Document.COLUMN_DOCUMENT_ID, rowId);
                 candidateCursor.close();
             }
 
             mDatabase.setTransactionSuccessful();
-            return documentIds;
+            return added;
         } finally {
             mDatabase.endTransaction();
         }
     }
 
+    /**
+     * Puts extra information for root documents.
+     * @param values Values containing extra information.
+     */
     void putRootExtra(ContentValues values) {
         mDatabase.replace(TABLE_ROOT_EXTRA, null, values);
     }
@@ -199,8 +232,9 @@ class MtpDatabaseInternal {
      * @param selection Query to select rows for resolving.
      * @param arg Argument for selection SQL.
      * @param groupKey Column name used to find corresponding rows.
+     * @return Whether the methods adds or removed visible rows.
      */
-    void stopAddingDocuments(String selection, String arg, String groupKey) {
+    boolean stopAddingDocuments(String selection, String arg, String groupKey) {
         mDatabase.beginTransaction();
         try {
             // Get 1-to-1 mapping of invalidated document and pending document.
@@ -265,22 +299,29 @@ class MtpDatabaseInternal {
             }
             mergingCursor.close();
 
+            boolean changed = false;
+
             // Delete all invalidated rows that cannot be mapped.
-            deleteDocumentsAndRoots(
+            if (deleteDocumentsAndRoots(
                     COLUMN_ROW_STATE + " = ? AND " + selection,
-                    strings(ROW_STATE_INVALIDATED, arg));
+                    strings(ROW_STATE_INVALIDATED, arg))) {
+                changed = true;
+            }
 
             // 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(
+            if (mDatabase.update(
                     TABLE_DOCUMENTS,
                     values,
                     COLUMN_ROW_STATE + " = ? AND " + selection,
-                    strings(ROW_STATE_PENDING, arg));
+                    strings(ROW_STATE_PENDING, arg)) != 0) {
+                changed = true;
+            }
             mDatabase.setTransactionSuccessful();
+            return changed;
         } finally {
             mDatabase.endTransaction();
         }
@@ -307,14 +348,23 @@ class MtpDatabaseInternal {
         }
     }
 
+    /**
+     * {@link android.database.sqlite.SQLiteDatabase#beginTransaction()}
+     */
     void beginTransaction() {
         mDatabase.beginTransaction();
     }
 
+    /**
+     * {@link android.database.sqlite.SQLiteDatabase#setTransactionSuccessful()}
+     */
     void setTransactionSuccessful() {
         mDatabase.setTransactionSuccessful();
     }
 
+    /**
+     * {@link android.database.sqlite.SQLiteDatabase#endTransaction()}
+     */
     void endTransaction() {
         mDatabase.endTransaction();
     }
@@ -323,11 +373,13 @@ class MtpDatabaseInternal {
      * 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.
+     * @return Whether the method deletes rows.
      */
-    private void deleteDocumentsAndRoots(String selection, String[] args) {
+    private boolean deleteDocumentsAndRoots(String selection, String[] args) {
         mDatabase.beginTransaction();
         try {
-            mDatabase.delete(
+            int deleted = 0;
+            deleted += mDatabase.delete(
                     TABLE_ROOT_EXTRA,
                     Root.COLUMN_ROOT_ID + " IN (" + SQLiteQueryBuilder.buildQueryString(
                             false,
@@ -339,8 +391,9 @@ class MtpDatabaseInternal {
                             null,
                             null) + ")",
                     args);
-            mDatabase.delete(TABLE_DOCUMENTS, selection, args);
+            deleted += mDatabase.delete(TABLE_DOCUMENTS, selection, args);
             mDatabase.setTransactionSuccessful();
+            return deleted != 0;
         } finally {
             mDatabase.endTransaction();
         }
index 7883e61..0931445 100644 (file)
@@ -62,6 +62,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
     private Map<Integer, DeviceToolkit> mDeviceToolkits;
     private RootScanner mRootScanner;
     private Resources mResources;
+    private MtpDatabase mDatabase;
 
     /**
      * Provides singleton instance to MtpDocumentsService.
@@ -77,17 +78,23 @@ public class MtpDocumentsProvider extends DocumentsProvider {
         mMtpManager = new MtpManager(getContext());
         mResolver = getContext().getContentResolver();
         mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
-        mRootScanner = new RootScanner(mResolver, mMtpManager);
+        mDatabase = new MtpDatabase(getContext());
+        mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
         return true;
     }
 
     @VisibleForTesting
-    void onCreateForTesting(Resources resources, MtpManager mtpManager, ContentResolver resolver) {
+    void onCreateForTesting(
+            Resources resources,
+            MtpManager mtpManager,
+            ContentResolver resolver,
+            MtpDatabase database) {
         mResources = resources;
         mMtpManager = mtpManager;
         mResolver = resolver;
         mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
-        mRootScanner = new RootScanner(mResolver, mMtpManager);
+        mDatabase = database;
+        mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
     }
 
     @Override
@@ -95,17 +102,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
         if (projection == null) {
             projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
         }
-        final MatrixCursor cursor = new MatrixCursor(projection);
-        final MtpRoot[] roots = mRootScanner.getRoots();
-        for (final MtpRoot root : roots) {
-            final Identifier rootIdentifier = new Identifier(root.mDeviceId, root.mStorageId);
-            final MatrixCursor.RowBuilder builder = cursor.newRow();
-            builder.add(Root.COLUMN_ROOT_ID, rootIdentifier.toRootId());
-            builder.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
-            builder.add(Root.COLUMN_TITLE, root.getRootName(mResources));
-            builder.add(Root.COLUMN_DOCUMENT_ID, rootIdentifier.toDocumentId());
-            builder.add(Root.COLUMN_AVAILABLE_BYTES , root.mFreeSpace);
-        }
+        final Cursor cursor = mDatabase.queryRoots(projection);
         cursor.setNotificationUri(
                 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
         return cursor;
@@ -266,14 +263,16 @@ public class MtpDocumentsProvider extends DocumentsProvider {
         // TODO: Flush the device before closing (if not closed externally).
         getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
         mDeviceToolkits.remove(deviceId);
+        mDatabase.removeDeviceRows(deviceId);
         mMtpManager.closeDevice(deviceId);
-        mRootScanner.scanNow();
+        mRootScanner.notifyChange();
     }
 
-    void closeAllDevices() {
+    void close() throws InterruptedException {
         boolean closed = false;
         for (int deviceId : mMtpManager.getOpenedDeviceIds()) {
             try {
+                mDatabase.removeDeviceRows(deviceId);
                 mMtpManager.closeDevice(deviceId);
                 getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
                 closed = true;
@@ -282,8 +281,10 @@ public class MtpDocumentsProvider extends DocumentsProvider {
             }
         }
         if (closed) {
-            mRootScanner.scanNow();
+            mRootScanner.notifyChange();
         }
+        mRootScanner.close();
+        mDatabase.close();
     }
 
     boolean hasOpenedDevices() {
index 328f618..2d1af26 100644 (file)
@@ -79,9 +79,13 @@ public class MtpDocumentsService extends Service {
     @Override
     public void onDestroy() {
         final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
-        provider.closeAllDevices();
         unregisterReceiver(mReceiver);
         mReceiver = null;
+        try {
+            provider.close();
+        } catch (InterruptedException e) {
+            Log.e(MtpDocumentsProvider.TAG, e.getMessage());
+        }
         super.onDestroy();
     }
 
index ffab176..415f89e 100644 (file)
@@ -1,14 +1,13 @@
 package com.android.mtp;
 
 import android.content.ContentResolver;
+import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Process;
 import android.provider.DocumentsContract;
 import android.util.Log;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
 
 final class RootScanner {
     /**
@@ -28,18 +27,28 @@ final class RootScanner {
     private final static long SHORT_POLLING_TIMES = 10;
 
     final ContentResolver mResolver;
+    final Resources mResources;
     final MtpManager mManager;
-    MtpRoot[] mLastRoots = new MtpRoot[0];
+    final MtpDatabase mDatabase;
+    boolean mClosed = false;
     int mPollingCount;
-    boolean mHasBackgroundTask = false;
+    Thread mBackgroundThread;
 
-    RootScanner(ContentResolver resolver, MtpManager manager) {
+    RootScanner(
+            ContentResolver resolver,
+            Resources resources,
+            MtpManager manager,
+            MtpDatabase database) {
         mResolver = resolver;
+        mResources = resources;
         mManager = manager;
+        mDatabase = database;
     }
 
-    synchronized MtpRoot[] getRoots() {
-        return mLastRoots;
+    void notifyChange() {
+        final Uri uri =
+                DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY);
+        mResolver.notifyChange(uri, null, false);
     }
 
     /**
@@ -47,26 +56,29 @@ final class RootScanner {
      * If the background thread has already gone, it restarts another background thread.
      */
     synchronized void scanNow() {
+        if (mClosed) {
+            return;
+        }
         mPollingCount = 0;
-        if (!mHasBackgroundTask) {
-            mHasBackgroundTask = true;
-            new BackgroundLoaderThread().start();
+        if (mBackgroundThread == null) {
+            mBackgroundThread = new BackgroundLoaderThread();
+            mBackgroundThread.start();
         } else {
             notify();
         }
     }
 
-    private MtpRoot[] createRoots(int[] deviceIds) {
-        final ArrayList<MtpRoot> roots = new ArrayList<>();
-        for (final int deviceId : deviceIds) {
-            try {
-                roots.addAll(Arrays.asList(mManager.getRoots(deviceId)));
-            } catch (IOException error) {
-                // Skip device that return error.
-                Log.d(MtpDocumentsProvider.TAG, error.getMessage());
+    void close() throws InterruptedException {
+        Thread thread;
+        synchronized (this) {
+            mClosed = true;
+            thread = mBackgroundThread;
+            if (mBackgroundThread == null) {
+                return;
             }
+            notify();
         }
-        return roots.toArray(new MtpRoot[roots.size()]);
+        thread.join();
     }
 
     private final class BackgroundLoaderThread extends Thread {
@@ -74,18 +86,28 @@ final class RootScanner {
         public void run() {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
             synchronized (RootScanner.this) {
-                while (true) {
+                while (!mClosed) {
                     final int[] deviceIds = mManager.getOpenedDeviceIds();
-                    final MtpRoot[] newRoots = createRoots(deviceIds);
-                    if (!Arrays.equals(mLastRoots, newRoots)) {
-                        final Uri uri =
-                                DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY);
-                        mResolver.notifyChange(uri, null, false);
-                        mLastRoots = newRoots;
-                    }
                     if (deviceIds.length == 0) {
                         break;
                     }
+                    boolean changed = false;
+                    for (int deviceId : deviceIds) {
+                        try {
+                            mDatabase.startAddingRootDocuments(deviceId);
+                            changed = mDatabase.putRootDocuments(
+                                    deviceId, mResources, mManager.getRoots(deviceId)) || changed;
+                            changed = mDatabase.stopAddingRootDocuments(deviceId) || changed;
+                        } catch (IOException exp) {
+                            // The error may happen on the device. We would like to continue getting
+                            // roots for other devices.
+                            Log.e(MtpDocumentsProvider.TAG, exp.getMessage());
+                            continue;
+                        }
+                    }
+                    if (changed) {
+                        notifyChange();
+                    }
                     mPollingCount++;
                     try {
                         // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is
@@ -99,7 +121,7 @@ final class RootScanner {
                     }
                 }
 
-                mHasBackgroundTask = false;
+                mBackgroundThread = null;
             }
         }
     }
index 05345e1..ce4cf14 100644 (file)
@@ -86,7 +86,7 @@ public class MtpDatabaseTest extends AndroidTestCase {
                     Root.COLUMN_ICON,
                     Root.COLUMN_TITLE,
                     Root.COLUMN_SUMMARY,
-                    Root.COLUMN_DOCUMENT_ID,
+                    Root.COLUMN_DOCUMENT_ID + "_",
                     Root.COLUMN_AVAILABLE_BYTES,
                     Root.COLUMN_CAPACITY_BYTES
             });
index 5765f0a..bc7f28c 100644 (file)
@@ -36,13 +36,24 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
     private MtpDocumentsProvider mProvider;
     private TestMtpManager mMtpManager;
     private final TestResources mResources = new TestResources();
+    private MtpDatabase mDatabase;
 
     @Override
     public void setUp() throws IOException {
         mResolver = new TestContentResolver();
         mMtpManager = new TestMtpManager(getContext());
         mProvider = new MtpDocumentsProvider();
-        mProvider.onCreateForTesting(mResources, mMtpManager, mResolver);
+        mDatabase = new MtpDatabase(getContext());
+        mProvider.onCreateForTesting(mResources, mMtpManager, mResolver, mDatabase);
+    }
+
+    @Override
+    public void tearDown() {
+        try {
+            mProvider.close();
+        } catch (InterruptedException e) {
+            fail();
+        }
     }
 
     public void testOpenAndCloseDevice() throws Exception {
@@ -96,26 +107,6 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
         mResolver.waitForNotification(ROOTS_URI, 1);
     }
 
-    public void testCloseAllDevices() throws Exception {
-        mMtpManager.addValidDevice(0);
-        mMtpManager.setRoots(0, new MtpRoot[] {
-                new MtpRoot(
-                        0 /* deviceId */,
-                        1 /* storageId */,
-                        "Device A" /* device model name */,
-                        "Storage A" /* volume description */,
-                        1024 /* free space */,
-                        2048 /* total space */,
-                        "" /* no volume identifier */)
-        });
-        mProvider.closeAllDevices();
-        mProvider.openDevice(0);
-        mResolver.waitForNotification(ROOTS_URI, 1);
-
-        mProvider.closeAllDevices();
-        mResolver.waitForNotification(ROOTS_URI, 2);
-    }
-
     public void testQueryRoots() throws Exception {
         mMtpManager.addValidDevice(0);
         mMtpManager.addValidDevice(1);
@@ -146,7 +137,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
             final Cursor cursor = mProvider.queryRoots(null);
             assertEquals(1, cursor.getCount());
             cursor.moveToNext();
-            assertEquals("0_1", cursor.getString(0));
+            assertEquals("1", cursor.getString(0));
             assertEquals(Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE, cursor.getInt(1));
             // TODO: Add storage icon for MTP devices.
             assertTrue(cursor.isNull(2) /* icon */);
@@ -162,7 +153,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
             assertEquals(2, cursor.getCount());
             cursor.moveToNext();
             cursor.moveToNext();
-            assertEquals("1_1", cursor.getString(0));
+            assertEquals("2", cursor.getString(0));
             assertEquals(Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE, cursor.getInt(1));
             // TODO: Add storage icon for MTP devices.
             assertTrue(cursor.isNull(2) /* icon */);
@@ -170,13 +161,6 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
             assertEquals("1_1_0", cursor.getString(4));
             assertEquals(2048, cursor.getInt(5));
         }
-
-        {
-            mProvider.closeAllDevices();
-            mResolver.waitForNotification(ROOTS_URI, 3);
-            final Cursor cursor = mProvider.queryRoots(null);
-            assertEquals(0, cursor.getCount());
-        }
     }
 
     public void testQueryRoots_error() throws Exception {
@@ -201,7 +185,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
             final Cursor cursor = mProvider.queryRoots(null);
             assertEquals(1, cursor.getCount());
             cursor.moveToNext();
-            assertEquals("1_1", cursor.getString(0));
+            assertEquals("1", cursor.getString(0));
             assertEquals(Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE, cursor.getInt(1));
             // TODO: Add storage icon for MTP devices.
             assertTrue(cursor.isNull(2) /* icon */);