Merge "Reopen MTP devices when the provider is created."
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
index 4afffea..00fe7a7 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabase.java
@@ -22,6 +22,7 @@
 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;
@@ -34,6 +35,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.Objects;
 
@@ -305,6 +307,32 @@
         }
     }
 
+    /**
+     * Returns the set of device ID stored in the database.
+     */
+    int[] getDeviceIds() {
+        final Cursor cursor = mDatabase.query(
+                true,
+                TABLE_DOCUMENTS,
+                strings(COLUMN_DEVICE_ID),
+                null,
+                null,
+                null,
+                null,
+                null,
+                null);
+        try {
+            final int[] ids = new int[cursor.getCount()];
+            for (int i = 0; i < ids.length; i++) {
+                cursor.moveToNext();
+                ids[i] = cursor.getInt(0);
+            }
+            return ids;
+        } finally {
+            cursor.close();
+        }
+    }
+
     private boolean deleteDocumentsAndRoots(String selection, String[] args) {
         mDatabase.beginTransaction();
         try {
@@ -351,6 +379,11 @@
         }
     }
 
+    @VisibleForTesting
+    static void deleteDatabase(Context context) {
+        context.deleteDatabase(DATABASE_NAME);
+    }
+
     /**
      * Gets {@link ContentValues} for the given root.
      * @param values {@link ContentValues} that receives values.
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
index b0286aa..0ead2d5 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDatabaseConstants.java
@@ -24,7 +24,7 @@
  */
 class MtpDatabaseConstants {
     static final int DATABASE_VERSION = 1;
-    static final String DATABASE_NAME = null;
+    static final String DATABASE_NAME = "database";
 
     static final int FLAG_DATABASE_IN_MEMORY = 1;
     static final int FLAG_DATABASE_IN_FILE = 0;
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 743f583..9511e15 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -20,7 +20,6 @@
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.database.MatrixCursor;
 import android.graphics.Point;
 import android.media.MediaFile;
 import android.mtp.MtpConstants;
@@ -58,11 +57,13 @@
             Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     };
 
+    private final Object mDeviceListLock = new Object();
+
     private static MtpDocumentsProvider sSingleton;
 
     private MtpManager mMtpManager;
     private ContentResolver mResolver;
-    @GuardedBy("mDeviceToolkits")
+    @GuardedBy("mDeviceListLock")
     private Map<Integer, DeviceToolkit> mDeviceToolkits;
     private RootScanner mRootScanner;
     private Resources mResources;
@@ -84,6 +85,7 @@
         mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
         mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
         mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
+        resume();
         return true;
     }
 
@@ -99,6 +101,7 @@
         mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
         mDatabase = database;
         mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
+        resume();
     }
 
     @Override
@@ -196,7 +199,7 @@
 
     @Override
     public void onTrimMemory(int level) {
-        synchronized (mDeviceToolkits) {
+        synchronized (mDeviceListLock) {
             for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
                 toolkit.mDocumentLoader.clearCompletedTasks();
             }
@@ -234,47 +237,26 @@
     }
 
     void openDevice(int deviceId) throws IOException {
-        synchronized (mDeviceToolkits) {
+        synchronized (mDeviceListLock) {
             mMtpManager.openDevice(deviceId);
-            mDeviceToolkits.put(deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase));
+            mDeviceToolkits.put(
+                    deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase));
         }
         mRootScanner.resume();
     }
 
     void closeDevice(int deviceId) throws IOException, InterruptedException {
-        // TODO: Flush the device before closing (if not closed externally).
-        synchronized (mDeviceToolkits) {
-            getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
-            mDeviceToolkits.remove(deviceId);
+        synchronized (mDeviceListLock) {
+            closeDeviceInternal(deviceId);
             mDatabase.removeDeviceRows(deviceId);
-            mMtpManager.closeDevice(deviceId);
         }
         mRootScanner.notifyChange();
-        if (!hasOpenedDevices()) {
-            mRootScanner.pause();
-        }
-    }
-
-    synchronized void closeAllDevices() throws InterruptedException {
-        boolean closed = false;
-        for (int deviceId : mMtpManager.getOpenedDeviceIds()) {
-            try {
-                mDatabase.removeDeviceRows(deviceId);
-                mMtpManager.closeDevice(deviceId);
-                getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
-                closed = true;
-            } catch (IOException d) {
-                Log.d(TAG, "Failed to close the MTP device: " + deviceId);
-            }
-        }
-        if (closed) {
-            mRootScanner.notifyChange();
-            mRootScanner.pause();
-        }
     }
 
     boolean hasOpenedDevices() {
-        return mMtpManager.getOpenedDeviceIds().length != 0;
+        synchronized (mDeviceListLock) {
+            return mMtpManager.getOpenedDeviceIds().length != 0;
+        }
     }
 
     /**
@@ -282,14 +264,18 @@
      */
     @Override
     public void shutdown() {
-        try {
-            closeAllDevices();
-        } catch (InterruptedException e) {
-            // It should fail unit tests by throwing runtime exception.
-            throw new RuntimeException(e.getMessage());
-        } finally {
-            mDatabase.close();
-            super.shutdown();
+        synchronized (mDeviceListLock) {
+            try {
+                for (final int id : mMtpManager.getOpenedDeviceIds()) {
+                    closeDeviceInternal(id);
+                }
+            } catch (InterruptedException|IOException e) {
+                // It should fail unit tests by throwing runtime exception.
+                throw new RuntimeException(e);
+            } finally {
+                mDatabase.close();
+                super.shutdown();
+            }
         }
     }
 
@@ -300,8 +286,35 @@
                 false);
     }
 
+    /**
+     * Reopens MTP devices based on database state.
+     */
+    private void resume() {
+        synchronized (mDeviceListLock) {
+            mDatabase.getMapper().clearMapping();
+            final int[] ids = mDatabase.getDeviceIds();
+            for (final int id : ids) {
+                try {
+                    openDevice(id);
+                } catch (IOException exception) {
+                    mDatabase.removeDeviceRows(id);
+                }
+            }
+        }
+    }
+
+    private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
+        // TODO: Flush the device before closing (if not closed externally).
+        getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
+        mDeviceToolkits.remove(deviceId);
+        mMtpManager.closeDevice(deviceId);
+        if (!hasOpenedDevices()) {
+            mRootScanner.pause();
+        }
+    }
+
     private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
-        synchronized (mDeviceToolkits) {
+        synchronized (mDeviceListLock) {
             final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
             if (toolkit == null) {
                 throw new FileNotFoundException();
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
index 723dc14..9b3c20f 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
@@ -55,22 +55,20 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        if (intent == null) {
-            // If intent is null, the service was restarted.
-            // TODO: Recover opened devices here.
-            return START_STICKY;
-        }
-        if (intent.getAction().equals(ACTION_OPEN_DEVICE)) {
-            final UsbDevice device = intent.<UsbDevice>getParcelableExtra(EXTRA_DEVICE);
-            try {
-                final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
-                provider.openDevice(device.getDeviceId());
-                return START_STICKY;
-            } catch (IOException error) {
-                Log.e(MtpDocumentsProvider.TAG, error.getMessage());
+        // If intent is null, the service was restarted.
+        if (intent != null) {
+            if (intent.getAction().equals(ACTION_OPEN_DEVICE)) {
+                final UsbDevice device = intent.<UsbDevice>getParcelableExtra(EXTRA_DEVICE);
+                try {
+                    final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
+                    provider.openDevice(device.getDeviceId());
+                    return START_STICKY;
+                } catch (IOException error) {
+                    Log.e(MtpDocumentsProvider.TAG, error.getMessage());
+                }
+            } else {
+                Log.e(MtpDocumentsProvider.TAG, "Received unknown intent action.");
             }
-        } else {
-            Log.w(MtpDocumentsProvider.TAG, "Received unknown intent action.");
         }
         stopSelfIfNeeded();
         return Service.START_NOT_STICKY;
@@ -78,14 +76,8 @@
 
     @Override
     public void onDestroy() {
-        final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
         unregisterReceiver(mReceiver);
         mReceiver = null;
-        try {
-            provider.closeAllDevices();
-        } catch (InterruptedException e) {
-            Log.e(MtpDocumentsProvider.TAG, e.getMessage());
-        }
         super.onDestroy();
     }
 
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
index 714936d..cd52f31 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
@@ -32,6 +32,7 @@
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.ArrayList;
 
 /**
  * The model wrapping android.mtp API.
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
index 7a53a90..b20b3bb 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
@@ -23,6 +23,7 @@
 import android.provider.DocumentsContract.Root;
 import android.provider.DocumentsContract;
 import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import java.io.FileNotFoundException;
@@ -45,17 +46,16 @@
     public void setUp() throws IOException {
         mResolver = new TestContentResolver();
         mMtpManager = new TestMtpManager(getContext());
-        mProvider = new MtpDocumentsProvider();
-        mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
-        mProvider.onCreateForTesting(mResources, mMtpManager, mResolver, mDatabase);
     }
 
     @Override
     public void tearDown() {
         mProvider.shutdown();
+        MtpDatabase.deleteDatabase(getContext());
     }
 
     public void testOpenAndCloseDevice() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mMtpManager.setRoots(0, new MtpRoot[] {
                 new MtpRoot(
@@ -76,6 +76,7 @@
     }
 
     public void testOpenAndCloseErrorDevice() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         try {
             mProvider.openDevice(1);
             fail();
@@ -107,6 +108,7 @@
     }
 
     public void testQueryRoots() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mMtpManager.addValidDevice(1);
         mMtpManager.setRoots(0, new MtpRoot[] {
@@ -163,6 +165,7 @@
     }
 
     public void testQueryRoots_error() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mMtpManager.addValidDevice(1);
         // Not set roots for device 0 so that MtpManagerMock#getRoots throws IOException.
@@ -195,6 +198,7 @@
     }
 
     public void testQueryDocument() throws IOException, InterruptedException, TimeoutException {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
 
@@ -234,6 +238,7 @@
 
     public void testQueryDocument_directory()
             throws IOException, InterruptedException, TimeoutException {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
 
@@ -271,6 +276,7 @@
 
     public void testQueryDocument_forRoot()
             throws IOException, InterruptedException, TimeoutException {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
 
@@ -297,6 +303,7 @@
     }
 
     public void testQueryChildDocuments() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
 
@@ -332,6 +339,7 @@
     }
 
     public void testQueryChildDocuments_cursorError() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
         try {
@@ -343,6 +351,7 @@
     }
 
     public void testQueryChildDocuments_documentError() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
         setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Device", "Storage", 1000, 1000, "") });
@@ -356,6 +365,7 @@
     }
 
     public void testDeleteDocument() throws IOException, InterruptedException, TimeoutException {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
         setupRoots(0, new MtpRoot[] {
@@ -377,6 +387,7 @@
 
     public void testDeleteDocument_error()
             throws IOException, InterruptedException, TimeoutException {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
         mMtpManager.addValidDevice(0);
         mProvider.openDevice(0);
         setupRoots(0, new MtpRoot[] {
@@ -400,6 +411,40 @@
                         MtpDocumentsProvider.AUTHORITY, "1")));
     }
 
+    @MediumTest
+    public void testPauseAndResume() throws Exception {
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
+        mMtpManager.addValidDevice(0);
+        mProvider.openDevice(0);
+        setupRoots(0, new MtpRoot[] { new MtpRoot(0, 0, "Device", "Storage", 0, 0, "")});
+
+        {
+            final Cursor cursor = mProvider.queryRoots(
+                    strings(DocumentsContract.Root.COLUMN_ROOT_ID));
+            cursor.moveToNext();
+            assertEquals(1, cursor.getInt(0));
+        }
+
+        mProvider.shutdown();
+        setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
+
+        {
+            // We can still fetch roots after relaunching the provider.
+            final Cursor cursor = mProvider.queryRoots(
+                    strings(DocumentsContract.Root.COLUMN_ROOT_ID));
+            assertEquals(1, cursor.getCount());
+            cursor.moveToNext();
+            assertEquals(1, cursor.getInt(0));
+            assertEquals(1, mMtpManager.getOpenedDeviceIds().length);
+        }
+    }
+
+    private void setupProvider(int flag) {
+        mDatabase = new MtpDatabase(getContext(), flag);
+        mProvider = new MtpDocumentsProvider();
+        mProvider.onCreateForTesting(mResources, mMtpManager, mResolver, mDatabase);
+    }
+
     private String[] getStrings(Cursor cursor) {
         try {
             final String[] results = new String[cursor.getCount()];