Add openDevice and closeDevice methods to MtpDocumentsProvider.

These methods will be called in the following CL from the service that handles
USB attach and detach intent.

BUG=20274999

Change-Id: I7b3c658afc5750d6a2713c07f40c59b26dcd1460
diff --git a/packages/MtpDocumentsProvider/Android.mk b/packages/MtpDocumentsProvider/Android.mk
index ec18463..5f76a9a 100644
--- a/packages/MtpDocumentsProvider/Android.mk
+++ b/packages/MtpDocumentsProvider/Android.mk
@@ -5,6 +5,8 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_PACKAGE_NAME := MtpDocumentsProvider
 LOCAL_CERTIFICATE := media
+LOCAL_PROGUARD_FLAGS := '-keepclassmembers class * {' \
+    ' @com.android.internal.annotations.VisibleForTesting *; }'
 
 include $(BUILD_PACKAGE)
 include $(LOCAL_PATH)/tests/Android.mk
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index ba3067e..0cfa749 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -16,14 +16,24 @@
 
 package com.android.mtp;
 
+import android.content.ContentResolver;
 import android.database.Cursor;
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
 import android.provider.DocumentsProvider;
-import java.io.FileNotFoundException;
+import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * DocumentsProvider for MTP devices.
+ */
 public class MtpDocumentsProvider extends DocumentsProvider {
     private static final String TAG = "MtpDocumentsProvider";
     public static final String AUTHORITY = "com.android.mtp.documents";
@@ -40,32 +50,89 @@
             Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     };
 
+    private MtpManager mDeviceModel;
+    private ContentResolver mResolver;
+
     @Override
     public boolean onCreate() {
+        mDeviceModel = new MtpManager(getContext());
+        mResolver = getContext().getContentResolver();
         return true;
     }
 
+    @VisibleForTesting
+    void onCreateForTesting(MtpManager deviceModel, ContentResolver resolver) {
+        this.mDeviceModel = deviceModel;
+        this.mResolver = resolver;
+    }
+
     @Override
     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
-        return null;
+        throw new FileNotFoundException();
     }
 
     @Override
     public Cursor queryDocument(String documentId, String[] projection)
             throws FileNotFoundException {
-        return null;
+        throw new FileNotFoundException();
     }
 
     @Override
     public Cursor queryChildDocuments(String parentDocumentId,
             String[] projection, String sortOrder)
             throws FileNotFoundException {
-        return null;
+        throw new FileNotFoundException();
     }
 
     @Override
     public ParcelFileDescriptor openDocument(String documentId, String mode,
             CancellationSignal signal) throws FileNotFoundException {
-        return null;
+        throw new FileNotFoundException();
+    }
+
+    // TODO: Remove annotation when the method starts to be used.
+    @VisibleForTesting
+    void openDevice(int deviceId) {
+        try {
+            mDeviceModel.openDevice(deviceId);
+            notifyRootsUpdate();
+        } catch (IOException error) {
+            Log.d(TAG, "Failed to open the MTP device: " + deviceId);
+        }
+    }
+
+    // TODO: Remove annotation when the method starts to be used.
+    @VisibleForTesting
+    void closeDevice(int deviceId) {
+        try {
+            mDeviceModel.closeDevice(deviceId);
+            notifyRootsUpdate();
+        } catch (IOException error) {
+            Log.d(TAG, "Failed to close the MTP device: " + deviceId);
+        }
+    }
+
+    // TODO: Remove annotation when the method starts to be used.
+    @VisibleForTesting
+    void closeAllDevices() {
+        boolean closed = false;
+        for (int deviceId : mDeviceModel.getOpenedDeviceIds()) {
+            try {
+                mDeviceModel.closeDevice(deviceId);
+                closed = true;
+            } catch (IOException d) {
+                Log.d(TAG, "Failed to close the MTP device: " + deviceId);
+            }
+        }
+        if (closed) {
+            notifyRootsUpdate();
+        }
+    }
+
+    private void notifyRootsUpdate() {
+        mResolver.notifyChange(
+                DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY),
+                null,
+                false);
     }
 }
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
new file mode 100644
index 0000000..eea38b1
--- /dev/null
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mtp;
+
+import android.content.Context;
+
+import java.io.IOException;
+
+/**
+ * The model wrapping android.mtp API.
+ */
+class MtpManager {
+    MtpManager(Context context) {
+    }
+
+    void openDevice(int deviceId) throws IOException {
+        // TODO: Implement this method.
+    }
+
+    void closeDevice(int deviceId) throws IOException {
+        // TODO: Implement this method.
+    }
+
+    int[] getOpenedDeviceIds() {
+        // TODO: Implement this method.
+        return null;
+    }
+}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
index 990d6ae..7e91cc7 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
@@ -16,13 +16,92 @@
 
 package com.android.mtp;
 
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
 import android.test.AndroidTestCase;
+import android.test.mock.MockContentResolver;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import java.io.IOException;
+
 @SmallTest
 public class MtpDocumentsProviderTest extends AndroidTestCase {
-    public void testBasic() {
+    public void testOpenAndCloseDevice() throws Exception {
+        final ContentResolver resolver = new ContentResolver();
         final MtpDocumentsProvider provider = new MtpDocumentsProvider();
-        assertNotNull(provider);
+        provider.onCreateForTesting(new MtpManagerMock(getContext()), resolver);
+
+        provider.openDevice(MtpManagerMock.SUCCESS_DEVICE_ID);
+        assertEquals(1, resolver.changeCount);
+        provider.closeDevice(MtpManagerMock.SUCCESS_DEVICE_ID);
+        assertEquals(2, resolver.changeCount);
+
+        provider.openDevice(MtpManagerMock.FAILURE_DEVICE_ID);
+        assertEquals(2, resolver.changeCount);
+        provider.closeDevice(MtpManagerMock.FAILURE_DEVICE_ID);
+        assertEquals(2, resolver.changeCount);
+    }
+
+    public void testCloseAllDevices() {
+        final ContentResolver resolver = new ContentResolver();
+        final MtpDocumentsProvider provider = new MtpDocumentsProvider();
+        provider.onCreateForTesting(new MtpManagerMock(getContext()), resolver);
+
+        provider.closeAllDevices();
+        assertEquals(0, resolver.changeCount);
+
+        provider.openDevice(MtpManagerMock.SUCCESS_DEVICE_ID);
+        assertEquals(1, resolver.changeCount);
+
+        provider.closeAllDevices();
+        assertEquals(2, resolver.changeCount);
+    }
+
+    private static class MtpManagerMock extends MtpManager {
+        final static int SUCCESS_DEVICE_ID = 1;
+        final static int FAILURE_DEVICE_ID = 2;
+
+        private boolean opened = false;
+
+        MtpManagerMock(Context context) {
+            super(context);
+        }
+
+        @Override
+        void openDevice(int deviceId) throws IOException {
+            if (deviceId == SUCCESS_DEVICE_ID) {
+                opened = true;
+            } else {
+                throw new IOException();
+            }
+        }
+
+        @Override
+        void closeDevice(int deviceId) throws IOException {
+            if (opened && deviceId == SUCCESS_DEVICE_ID) {
+                opened = false;
+            } else {
+                throw new IOException();
+            }
+        }
+
+        @Override
+        int[] getOpenedDeviceIds() {
+            if (opened) {
+                return new int[] { SUCCESS_DEVICE_ID };
+            } else {
+                return new int[0];
+            }
+        }
+    }
+
+    private static class ContentResolver extends MockContentResolver {
+        int changeCount = 0;
+
+        @Override
+        public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+            changeCount++;
+        }
     }
 }