From fda7474c5faae1e36a9274d8a5fe83e42ec6503b Mon Sep 17 00:00:00 2001 From: Daichi Hirono Date: Mon, 1 Feb 2016 13:00:31 +0900 Subject: [PATCH] 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 --- .../MtpDocumentsProvider/AndroidManifest.xml | 6 +- .../com/android/mtp/MtpDocumentsProvider.java | 36 ++++++++- .../com/android/mtp/MtpDocumentsService.java | 35 ++------ .../src/com/android/mtp/RootScanner.java | 27 ++++++- .../com/android/mtp/ServiceIntentSender.java | 38 +++++++++ .../com/android/mtp/UsbIntentReceiver.java | 18 ++--- .../android/mtp/MtpDocumentsProviderTest.java | 81 +++++++++++++++---- .../android/mtp/TestServiceIntentSender.java | 26 ++++++ .../server/usb/MtpNotificationManager.java | 1 - 9 files changed, 203 insertions(+), 65 deletions(-) create mode 100644 packages/MtpDocumentsProvider/src/com/android/mtp/ServiceIntentSender.java create mode 100644 packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestServiceIntentSender.java diff --git a/packages/MtpDocumentsProvider/AndroidManifest.xml b/packages/MtpDocumentsProvider/AndroidManifest.xml index 7ea54c9774bf3..2dd49ab0782f8 100644 --- a/packages/MtpDocumentsProvider/AndroidManifest.xml +++ b/packages/MtpDocumentsProvider/AndroidManifest.xml @@ -30,16 +30,12 @@ - + - - - diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java index 775f9766e2499..20abf2eab6aca 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java @@ -70,6 +70,7 @@ public class MtpDocumentsProvider extends DocumentsProvider { private Resources mResources; private MtpDatabase mDatabase; private AppFuse mAppFuse; + private ServiceIntentSender mIntentSender; /** * Provides singleton instance to MtpDocumentsService. @@ -88,6 +89,7 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { MtpManager mtpManager, ContentResolver resolver, MtpDatabase database, - StorageManager storageManager) { + StorageManager storageManager, + ServiceIntentSender intentSender) { mResources = resources; mMtpManager = mtpManager; mResolver = resolver; @@ -113,6 +116,7 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { } 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 class MtpDocumentsProvider extends DocumentsProvider { 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 @@ public class MtpDocumentsProvider extends DocumentsProvider { closeDeviceInternal(deviceId); } mRootScanner.resume(); + mIntentSender.sendUpdateNotificationIntent(); } int[] getOpenedDeviceIds() { @@ -317,6 +335,13 @@ public class MtpDocumentsProvider extends DocumentsProvider { } } + /** + * Resumes root scanner to handle the update of device list. + */ + void resumeRootScanner() { + mRootScanner.resume(); + } + /** * Finalize the content provider for unit tests. */ @@ -356,6 +381,9 @@ public class MtpDocumentsProvider extends DocumentsProvider { 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 5bede8617d600..9c4952bc57590 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsService.java @@ -19,13 +19,12 @@ package com.android.mtp; 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 @@ import java.io.IOException; 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 @@ public class MtpDocumentsService extends Service { @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.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 @@ public class MtpDocumentsService extends Service { 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 15b8ef3cae9a5..39bea49ea6759 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.os.Process; 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 @@ final class RootScanner { /** * 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 @@ final class RootScanner { // Cancel previous task. mCurrentTask.cancel(true); } - mCurrentTask = new FutureTask(new UpdateRootsRunnable(), null); + final UpdateRootsRunnable runnable = new UpdateRootsRunnable(); + mCurrentTask = new FutureTask(runnable, null); mExecutor.submit(mCurrentTask); + return runnable.mFirstScanCompleted; } /** @@ -98,6 +116,8 @@ final class RootScanner { * 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 @@ final class RootScanner { 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 0000000000000..a1bb2c13d3860 --- /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 0ac130eb7ec0d..0489ea8633966 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.Context; 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 @@ public class UsbIntentReceiver extends BroadcastReceiver { 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 360661270240a..d9e928b7142e3 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.net.Uri; 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 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { 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 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { } 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 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { }, 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 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { 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 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { 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 @@ public class MtpDocumentsProviderTest extends AndroidTestCase { 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 0000000000000..d4a4a487d8ee1 --- /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() {} +} diff --git a/services/usb/java/com/android/server/usb/MtpNotificationManager.java b/services/usb/java/com/android/server/usb/MtpNotificationManager.java index 203d35ed1fc4e..17039bb6870cd 100644 --- a/services/usb/java/com/android/server/usb/MtpNotificationManager.java +++ b/services/usb/java/com/android/server/usb/MtpNotificationManager.java @@ -30,7 +30,6 @@ import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbManager; import android.os.UserHandle; -import android.util.Log; /** * Manager for MTP storage notification.