Open MTP device on demand.
Previously MtpDocumentsProvider opens a device just after device is
connected to Android. But MtpDocumentsProvider should open MTP device on
demand so that other applications can open device if user starts to use
the application before using MtpDocumentsProvider.
BUG=26625708
Change-Id: I6083b8c7cef49ee6e9fb0d15ca4adc129734f3eb
diff --git a/packages/MtpDocumentsProvider/AndroidManifest.xml b/packages/MtpDocumentsProvider/AndroidManifest.xml
index 7ea54c9..2dd49ab 100644
--- a/packages/MtpDocumentsProvider/AndroidManifest.xml
+++ b/packages/MtpDocumentsProvider/AndroidManifest.xml
@@ -30,16 +30,12 @@
</activity>
<receiver android:name=".UsbIntentReceiver" android:exported="true">
- <!-- TODO: Remove an intent-filter for USB_DEVICE_ATTACHED after the provider becomes to
- open devices on demand. -->
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+ <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
- <intent-filter>
- <action android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />
- </intent-filter>
</receiver>
</application>
</manifest>
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 775f976..20abf2e 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -70,6 +70,7 @@
private Resources mResources;
private MtpDatabase mDatabase;
private AppFuse mAppFuse;
+ private ServiceIntentSender mIntentSender;
/**
* Provides singleton instance to MtpDocumentsService.
@@ -88,6 +89,7 @@
mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
mAppFuse = new AppFuse(TAG, new AppFuseCallback());
+ mIntentSender = new ServiceIntentSender(getContext());
// TODO: Mount AppFuse on demands.
try {
mAppFuse.mount(getContext().getSystemService(StorageManager.class));
@@ -105,7 +107,8 @@
MtpManager mtpManager,
ContentResolver resolver,
MtpDatabase database,
- StorageManager storageManager) {
+ StorageManager storageManager,
+ ServiceIntentSender intentSender) {
mResources = resources;
mMtpManager = mtpManager;
mResolver = resolver;
@@ -113,6 +116,7 @@
mDatabase = database;
mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
mAppFuse = new AppFuse(TAG, new AppFuseCallback());
+ mIntentSender = intentSender;
// TODO: Mount AppFuse on demands.
try {
mAppFuse.mount(storageManager);
@@ -152,6 +156,7 @@
}
Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
try {
+ openDevice(parentIdentifier.mDeviceId);
if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
final Identifier singleStorageIdentifier =
mDatabase.getSingleStorageIdentifier(parentDocumentId);
@@ -176,11 +181,12 @@
throws FileNotFoundException {
final Identifier identifier = mDatabase.createIdentifier(documentId);
try {
+ openDevice(identifier.mDeviceId);
switch (mode) {
case "r":
final long fileSize = getFileSize(documentId);
- // MTP getPartialObject operation does not support files that are larger than 4GB.
- // Fallback to non-seekable file descriptor.
+ // MTP getPartialObject operation does not support files that are larger than
+ // 4GB. Fallback to non-seekable file descriptor.
// TODO: Use getPartialObject64 for MTP devices that support Android vendor
// extension.
if (fileSize <= 0xffffffffl) {
@@ -213,6 +219,7 @@
CancellationSignal signal) throws FileNotFoundException {
final Identifier identifier = mDatabase.createIdentifier(documentId);
try {
+ openDevice(identifier.mDeviceId);
return new AssetFileDescriptor(
getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
0, // Start offset.
@@ -227,6 +234,7 @@
public void deleteDocument(String documentId) throws FileNotFoundException {
try {
final Identifier identifier = mDatabase.createIdentifier(documentId);
+ openDevice(identifier.mDeviceId);
final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
mDatabase.deleteDocument(documentId);
@@ -259,6 +267,7 @@
throws FileNotFoundException {
try {
final Identifier parentId = mDatabase.createIdentifier(parentDocumentId);
+ openDevice(parentId.mDeviceId);
final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe();
pipe[0].close(); // 0 bytes for a new document.
final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
@@ -286,11 +295,19 @@
void openDevice(int deviceId) throws IOException {
synchronized (mDeviceListLock) {
+ if (mDeviceToolkits.containsKey(deviceId)) {
+ return;
+ }
mMtpManager.openDevice(deviceId);
mDeviceToolkits.put(
deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase));
+ mIntentSender.sendUpdateNotificationIntent();
+ try {
+ mRootScanner.resume().await();
+ } catch (InterruptedException error) {
+ Log.e(TAG, "openDevice", error);
+ }
}
- mRootScanner.resume();
}
void closeDevice(int deviceId) throws IOException, InterruptedException {
@@ -298,6 +315,7 @@
closeDeviceInternal(deviceId);
}
mRootScanner.resume();
+ mIntentSender.sendUpdateNotificationIntent();
}
int[] getOpenedDeviceIds() {
@@ -318,6 +336,13 @@
}
/**
+ * Resumes root scanner to handle the update of device list.
+ */
+ void resumeRootScanner() {
+ mRootScanner.resume();
+ }
+
+ /**
* Finalize the content provider for unit tests.
*/
@Override
@@ -356,6 +381,9 @@
private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
// TODO: Flush the device before closing (if not closed externally).
+ if (!mDeviceToolkits.containsKey(deviceId)) {
+ return;
+ }
getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
mDeviceToolkits.remove(deviceId);
mMtpManager.closeDevice(deviceId);
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
index 5bede86..9c4952b 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java
@@ -19,13 +19,12 @@
import android.app.Notification;
import android.app.Service;
import android.app.NotificationManager;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
-import android.hardware.usb.UsbDevice;
import android.os.IBinder;
import android.util.Log;
-import com.android.internal.util.Preconditions;
-
import java.io.IOException;
/**
@@ -36,6 +35,7 @@
public class MtpDocumentsService extends Service {
static final String ACTION_OPEN_DEVICE = "com.android.mtp.OPEN_DEVICE";
static final String ACTION_CLOSE_DEVICE = "com.android.mtp.CLOSE_DEVICE";
+ static final String ACTION_UPDATE_NOTIFICATION = "com.android.mtp.UPDATE_NOTIFICATION";
static final String EXTRA_DEVICE = "device";
NotificationManager mNotificationManager;
@@ -55,32 +55,10 @@
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// If intent is null, the service was restarted.
- if (intent != null) {
- final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
- final UsbDevice device = intent.<UsbDevice>getParcelableExtra(EXTRA_DEVICE);
- try {
- Preconditions.checkNotNull(device);
- switch (intent.getAction()) {
- case ACTION_OPEN_DEVICE:
- provider.openDevice(device.getDeviceId());
- break;
-
- case ACTION_CLOSE_DEVICE:
- mNotificationManager.cancel(device.getDeviceId());
- provider.closeDevice(device.getDeviceId());
- break;
-
- default:
- throw new IllegalArgumentException("Received unknown intent action.");
- }
- } catch (IOException | InterruptedException | IllegalArgumentException error) {
- logErrorMessage(error);
- }
- } else {
- // TODO: Fetch devices again.
+ if (intent == null || ACTION_UPDATE_NOTIFICATION.equals(intent.getAction())) {
+ return updateForegroundState() ? START_STICKY : START_NOT_STICKY;
}
-
- return updateForegroundState() ? START_STICKY : START_NOT_STICKY;
+ return START_NOT_STICKY;
}
/**
@@ -92,6 +70,7 @@
final int[] deviceIds = provider.getOpenedDeviceIds();
int notificationId = 0;
Notification notification = null;
+ // TODO: Hide notification if the device has already been removed.
for (final int deviceId : deviceIds) {
try {
final String title = getResources().getString(
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java b/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
index 15b8ef3..39bea49 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2016 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.ContentResolver;
@@ -7,6 +23,7 @@
import android.provider.DocumentsContract;
import android.util.Log;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
@@ -64,9 +81,8 @@
/**
* Starts to check new changes right away.
- * If the background thread has already gone, it restarts another background thread.
*/
- synchronized void resume() {
+ synchronized CountDownLatch resume() {
if (mExecutor == null) {
// Only single thread updates the database.
mExecutor = Executors.newSingleThreadExecutor();
@@ -75,8 +91,10 @@
// Cancel previous task.
mCurrentTask.cancel(true);
}
- mCurrentTask = new FutureTask<Void>(new UpdateRootsRunnable(), null);
+ final UpdateRootsRunnable runnable = new UpdateRootsRunnable();
+ mCurrentTask = new FutureTask<Void>(runnable, null);
mExecutor.submit(mCurrentTask);
+ return runnable.mFirstScanCompleted;
}
/**
@@ -98,6 +116,8 @@
* Runnable to scan roots and update the database information.
*/
private final class UpdateRootsRunnable implements Runnable {
+ final CountDownLatch mFirstScanCompleted = new CountDownLatch(1);
+
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
@@ -136,6 +156,7 @@
if (changed) {
notifyChange();
}
+ mFirstScanCompleted.countDown();
pollingCount++;
try {
// Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/ServiceIntentSender.java b/packages/MtpDocumentsProvider/src/com/android/mtp/ServiceIntentSender.java
new file mode 100644
index 0000000..a1bb2c1
--- /dev/null
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/ServiceIntentSender.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2016 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Sends intent to MtpDocumentsService.
+ */
+class ServiceIntentSender {
+ private Context mContext;
+
+ ServiceIntentSender(Context context) {
+ mContext = context;
+ }
+
+ void sendUpdateNotificationIntent() {
+ final Intent intent = new Intent(MtpDocumentsService.ACTION_UPDATE_NOTIFICATION);
+ intent.setComponent(new ComponentName(mContext, MtpDocumentsService.class));
+ mContext.startService(intent);
+ }
+}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/UsbIntentReceiver.java b/packages/MtpDocumentsProvider/src/com/android/mtp/UsbIntentReceiver.java
index 0ac130e..0489ea8 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/UsbIntentReceiver.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/UsbIntentReceiver.java
@@ -21,7 +21,9 @@
import android.content.Intent;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
-import android.net.Uri;
+import android.util.Log;
+
+import java.io.IOException;
public class UsbIntentReceiver extends BroadcastReceiver {
@Override
@@ -29,17 +31,15 @@
final UsbDevice device = intent.getExtras().getParcelable(UsbManager.EXTRA_DEVICE);
switch (intent.getAction()) {
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
- startService(context, MtpDocumentsService.ACTION_OPEN_DEVICE, device);
+ MtpDocumentsProvider.getInstance().resumeRootScanner();
break;
case UsbManager.ACTION_USB_DEVICE_DETACHED:
- startService(context, MtpDocumentsService.ACTION_CLOSE_DEVICE, device);
+ try {
+ MtpDocumentsProvider.getInstance().closeDevice(device.getDeviceId());
+ } catch (IOException | InterruptedException e) {
+ Log.e(MtpDocumentsProvider.TAG, "Failed to close device", e);
+ }
break;
}
}
-
- private void startService(Context context, String action, UsbDevice device) {
- final Intent intent = new Intent(action, Uri.EMPTY, context, MtpDocumentsService.class);
- intent.putExtra(MtpDocumentsService.EXTRA_DEVICE, device);
- context.startService(intent);
- }
}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
index 3606612..d9e928b 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
@@ -23,13 +23,11 @@
import android.os.ParcelFileDescriptor;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract.Root;
-import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.provider.DocumentsContract;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
-import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -79,11 +77,14 @@
null,
null));
- mProvider.openDevice(0);
+ mProvider.resumeRootScanner();
mResolver.waitForNotification(ROOTS_URI, 1);
- mProvider.closeDevice(0);
+ mProvider.openDevice(0);
mResolver.waitForNotification(ROOTS_URI, 2);
+
+ mProvider.closeDevice(0);
+ mResolver.waitForNotification(ROOTS_URI, 3);
}
public void testOpenAndCloseErrorDevice() throws Exception {
@@ -94,13 +95,7 @@
} catch (Throwable error) {
assertTrue(error instanceof IOException);
}
-
- try {
- mProvider.closeDevice(1);
- fail();
- } catch (Throwable error) {
- assertTrue(error instanceof IOException);
- }
+ assertEquals(0, mProvider.getOpenedDeviceIds().length);
// Check if the following notification is the first one or not.
mMtpManager.addValidDevice(new MtpDeviceRecord(
@@ -119,8 +114,55 @@
},
null,
null));
- mProvider.openDevice(0);
+ mProvider.resumeRootScanner();
mResolver.waitForNotification(ROOTS_URI, 1);
+ mProvider.openDevice(0);
+ mResolver.waitForNotification(ROOTS_URI, 2);
+ }
+
+ public void testOpenDeviceOnDemand() throws Exception {
+ setupProvider(MtpDatabaseConstants.FLAG_DATABASE_IN_MEMORY);
+ mMtpManager.addValidDevice(new MtpDeviceRecord(
+ 0,
+ "Device A",
+ false /* unopened */,
+ 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 */)
+ },
+ null,
+ null));
+ mMtpManager.setObjectHandles(0, 1, -1, new int[0]);
+ mProvider.resumeRootScanner();
+ mResolver.waitForNotification(ROOTS_URI, 1);
+ final String[] columns = new String[] {
+ DocumentsContract.Root.COLUMN_TITLE,
+ DocumentsContract.Root.COLUMN_DOCUMENT_ID
+ };
+ try (final Cursor cursor = mProvider.queryRoots(columns)) {
+ assertEquals(1, cursor.getCount());
+ assertTrue(cursor.moveToNext());
+ assertEquals("Device A", cursor.getString(0));
+ assertEquals(1, cursor.getLong(1));
+ }
+ {
+ final int [] openedDevice = mProvider.getOpenedDeviceIds();
+ assertEquals(0, openedDevice.length);
+ }
+ // Device is opened automatically when querying its children.
+ try (final Cursor cursor = mProvider.queryChildDocuments("1", null, null)) {}
+
+ {
+ final int [] openedDevice = mProvider.getOpenedDeviceIds();
+ assertEquals(1, openedDevice.length);
+ assertEquals(0, openedDevice[0]);
+ }
}
public void testQueryRoots() throws Exception {
@@ -194,7 +236,7 @@
0, "Device A", false /* unopened */, new MtpRoot[0], null, null));
mMtpManager.addValidDevice(new MtpDeviceRecord(
1,
- "Device",
+ "Device B",
false /* unopened */,
new MtpRoot[] {
new MtpRoot(
@@ -210,9 +252,13 @@
null));
{
mProvider.openDevice(0);
- mProvider.openDevice(1);
+ mProvider.resumeRootScanner();
mResolver.waitForNotification(ROOTS_URI, 1);
+ mProvider.openDevice(1);
+ mProvider.resumeRootScanner();
+ mResolver.waitForNotification(ROOTS_URI, 2);
+
final Cursor cursor = mProvider.queryRoots(null);
assertEquals(2, cursor.getCount());
@@ -492,7 +538,12 @@
mProvider = new MtpDocumentsProvider();
final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
assertTrue(mProvider.onCreateForTesting(
- mResources, mMtpManager, mResolver, mDatabase, storageManager));
+ mResources,
+ mMtpManager,
+ mResolver,
+ mDatabase,
+ storageManager,
+ new TestServiceIntentSender()));
}
private String[] getStrings(Cursor cursor) {
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestServiceIntentSender.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestServiceIntentSender.java
new file mode 100644
index 0000000..d4a4a48
--- /dev/null
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestServiceIntentSender.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 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;
+
+class TestServiceIntentSender extends ServiceIntentSender {
+ TestServiceIntentSender() {
+ super(null);
+ }
+
+ @Override
+ void sendUpdateNotificationIntent() {}
+}