summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java222
-rw-r--r--packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java43
-rw-r--r--packages/MtpDocumentsProvider/tests/src/com/android/mtp/DocumentLoaderTest.java127
-rw-r--r--packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java25
-rw-r--r--packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestContentResolver.java57
-rw-r--r--packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java2
6 files changed, 424 insertions, 52 deletions
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java b/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java
new file mode 100644
index 000000000000..23532053a47f
--- /dev/null
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java
@@ -0,0 +1,222 @@
+/*
+ * 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.ContentResolver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.provider.DocumentsContract;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedList;
+
+class DocumentLoader {
+ static final int NUM_INITIAL_ENTRIES = 10;
+ static final int NUM_LOADING_ENTRIES = 20;
+ static final int NOTIFY_PERIOD_MS = 500;
+
+ private final MtpManager mMtpManager;
+ private final ContentResolver mResolver;
+ private final LinkedList<LoaderTask> mTasks = new LinkedList<LoaderTask>();
+ private boolean mHasBackgroundThread = false;
+
+ DocumentLoader(MtpManager mtpManager, ContentResolver resolver) {
+ mMtpManager = mtpManager;
+ mResolver = resolver;
+ }
+
+ private static MtpDocument[] loadDocuments(MtpManager manager, int deviceId, int[] handles)
+ throws IOException {
+ final MtpDocument[] documents = new MtpDocument[handles.length];
+ for (int i = 0; i < handles.length; i++) {
+ documents[i] = manager.getDocument(deviceId, handles[i]);
+ }
+ return documents;
+ }
+
+ synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent)
+ throws IOException {
+ LoaderTask task = findTask(parent);
+ if (task == null) {
+ int parentHandle = parent.mObjectHandle;
+ // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to
+ // getObjectHandles if we would like to obtain children under the root.
+ if (parentHandle == MtpDocument.DUMMY_HANDLE_FOR_ROOT) {
+ parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
+ }
+ task = new LoaderTask(parent, mMtpManager.getObjectHandles(
+ parent.mDeviceId, parent.mStorageId, parentHandle));
+ task.fillDocuments(loadDocuments(
+ mMtpManager,
+ parent.mDeviceId,
+ task.getUnloadedObjectHandles(NUM_INITIAL_ENTRIES)));
+ }
+
+ // Move this task to the head of the list to prioritize it.
+ mTasks.remove(task);
+ mTasks.addFirst(task);
+ if (!task.completed() && !mHasBackgroundThread) {
+ mHasBackgroundThread = true;
+ new BackgroundLoaderThread().start();
+ }
+
+ return task.createCursor(mResolver, columnNames);
+ }
+
+ synchronized void clearCache(int deviceId) {
+ int i = 0;
+ while (i < mTasks.size()) {
+ if (mTasks.get(i).mIdentifier.mDeviceId == deviceId) {
+ mTasks.remove(i);
+ } else {
+ i++;
+ }
+ }
+ }
+
+ synchronized void clearCache() {
+ int i = 0;
+ while (i < mTasks.size()) {
+ if (mTasks.get(i).completed()) {
+ mTasks.remove(i);
+ } else {
+ i++;
+ }
+ }
+ }
+
+ private LoaderTask findTask(Identifier parent) {
+ for (int i = 0; i < mTasks.size(); i++) {
+ if (mTasks.get(i).mIdentifier.equals(parent))
+ return mTasks.get(i);
+ }
+ return null;
+ }
+
+ private LoaderTask findUncompletedTask() {
+ for (int i = 0; i < mTasks.size(); i++) {
+ if (!mTasks.get(i).completed())
+ return mTasks.get(i);
+ }
+ return null;
+ }
+
+ private class BackgroundLoaderThread extends Thread {
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ while (true) {
+ LoaderTask task;
+ int deviceId;
+ int[] handles;
+ synchronized (DocumentLoader.this) {
+ task = findUncompletedTask();
+ if (task == null) {
+ mHasBackgroundThread = false;
+ return;
+ }
+ deviceId = task.mIdentifier.mDeviceId;
+ handles = task.getUnloadedObjectHandles(NUM_LOADING_ENTRIES);
+ }
+ MtpDocument[] documents;
+ try {
+ documents = loadDocuments(mMtpManager, deviceId, handles);
+ } catch (IOException exception) {
+ documents = null;
+ Log.d(MtpDocumentsProvider.TAG, exception.getMessage());
+ }
+ synchronized (DocumentLoader.this) {
+ if (documents != null) {
+ task.fillDocuments(documents);
+ final boolean shouldNotify =
+ task.mLastNotified.getTime() <
+ new Date().getTime() - NOTIFY_PERIOD_MS ||
+ task.completed();
+ if (shouldNotify) {
+ task.notify(mResolver);
+ }
+ } else {
+ mTasks.remove(task);
+ }
+ }
+ }
+ }
+ }
+
+ private static class LoaderTask {
+ final Identifier mIdentifier;
+ final int[] mObjectHandles;
+ final MtpDocument[] mDocuments;
+ Date mLastNotified;
+ int mNumLoaded;
+
+ LoaderTask(Identifier identifier, int[] objectHandles) {
+ mIdentifier = identifier;
+ mObjectHandles = objectHandles;
+ mDocuments = new MtpDocument[mObjectHandles.length];
+ mNumLoaded = 0;
+ mLastNotified = new Date();
+ }
+
+ Cursor createCursor(ContentResolver resolver, String[] columnNames) {
+ final MatrixCursor cursor = new MatrixCursor(columnNames);
+ final Identifier rootIdentifier = new Identifier(
+ mIdentifier.mDeviceId, mIdentifier.mStorageId);
+ for (int i = 0; i < mNumLoaded; i++) {
+ mDocuments[i].addToCursor(rootIdentifier, cursor.newRow());
+ }
+ final Bundle extras = new Bundle();
+ extras.putBoolean(DocumentsContract.EXTRA_LOADING, !completed());
+ cursor.setNotificationUri(resolver, createUri());
+ cursor.respond(extras);
+ return cursor;
+ }
+
+ boolean completed() {
+ return mNumLoaded == mDocuments.length;
+ }
+
+ int[] getUnloadedObjectHandles(int count) {
+ return Arrays.copyOfRange(
+ mObjectHandles,
+ mNumLoaded,
+ Math.min(mNumLoaded + count, mObjectHandles.length));
+ }
+
+ void notify(ContentResolver resolver) {
+ resolver.notifyChange(createUri(), null, false);
+ mLastNotified = new Date();
+ }
+
+ void fillDocuments(MtpDocument[] documents) {
+ for (int i = 0; i < documents.length; i++) {
+ mDocuments[mNumLoaded++] = documents[i];
+ }
+ }
+
+ private Uri createUri() {
+ return DocumentsContract.buildChildDocumentsUri(
+ MtpDocumentsProvider.AUTHORITY, mIdentifier.toDocumentId());
+ }
+ }
+}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 32882c8c8738..bcdfb714d241 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -40,12 +40,12 @@ import java.io.IOException;
public class MtpDocumentsProvider extends DocumentsProvider {
static final String AUTHORITY = "com.android.mtp.documents";
static final String TAG = "MtpDocumentsProvider";
- private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
+ static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES,
};
- private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+ static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
@@ -56,6 +56,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
private MtpManager mMtpManager;
private ContentResolver mResolver;
private PipeManager mPipeManager;
+ private DocumentLoader mDocumentLoader;
/**
* Provides singleton instance to MtpDocumentsService.
@@ -70,14 +71,15 @@ public class MtpDocumentsProvider extends DocumentsProvider {
mMtpManager = new MtpManager(getContext());
mResolver = getContext().getContentResolver();
mPipeManager = new PipeManager();
-
+ mDocumentLoader = new DocumentLoader(mMtpManager, mResolver);
return true;
}
@VisibleForTesting
void onCreateForTesting(MtpManager mtpManager, ContentResolver resolver) {
- this.mMtpManager = mtpManager;
- this.mResolver = resolver;
+ mMtpManager = mtpManager;
+ mResolver = resolver;
+ mDocumentLoader = new DocumentLoader(mMtpManager, mResolver);
}
@Override
@@ -152,7 +154,6 @@ public class MtpDocumentsProvider extends DocumentsProvider {
return cursor;
}
- // TODO: Support background loading for large number of files.
@Override
public Cursor queryChildDocuments(String parentDocumentId,
String[] projection, String sortOrder) throws FileNotFoundException {
@@ -160,29 +161,8 @@ public class MtpDocumentsProvider extends DocumentsProvider {
projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
}
final Identifier parentIdentifier = Identifier.createFromDocumentId(parentDocumentId);
- int parentHandle = parentIdentifier.mObjectHandle;
- // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to
- // getObjectHandles if we would like to obtain children under the root.
- if (parentHandle == MtpDocument.DUMMY_HANDLE_FOR_ROOT) {
- parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
- }
try {
- final MatrixCursor cursor = new MatrixCursor(projection);
- final Identifier rootIdentifier = new Identifier(
- parentIdentifier.mDeviceId, parentIdentifier.mStorageId);
- final int[] objectHandles = mMtpManager.getObjectHandles(
- parentIdentifier.mDeviceId, parentIdentifier.mStorageId, parentHandle);
- for (int i = 0; i < objectHandles.length; i++) {
- try {
- final MtpDocument document = mMtpManager.getDocument(
- parentIdentifier.mDeviceId, objectHandles[i]);
- document.addToCursor(rootIdentifier, cursor.newRow());
- } catch (IOException error) {
- cursor.close();
- throw new FileNotFoundException(error.getMessage());
- }
- }
- return cursor;
+ return mDocumentLoader.queryChildDocuments(projection, parentIdentifier);
} catch (IOException exception) {
throw new FileNotFoundException(exception.getMessage());
}
@@ -234,6 +214,11 @@ public class MtpDocumentsProvider extends DocumentsProvider {
}
}
+ @Override
+ public void onTrimMemory(int level) {
+ mDocumentLoader.clearCache();
+ }
+
void openDevice(int deviceId) throws IOException {
mMtpManager.openDevice(deviceId);
notifyRootsChange();
@@ -241,6 +226,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
void closeDevice(int deviceId) throws IOException {
mMtpManager.closeDevice(deviceId);
+ mDocumentLoader.clearCache(deviceId);
notifyRootsChange();
}
@@ -249,6 +235,7 @@ public class MtpDocumentsProvider extends DocumentsProvider {
for (int deviceId : mMtpManager.getOpenedDeviceIds()) {
try {
mMtpManager.closeDevice(deviceId);
+ mDocumentLoader.clearCache(deviceId);
closed = true;
} catch (IOException d) {
Log.d(TAG, "Failed to close the MTP device: " + deviceId);
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/DocumentLoaderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/DocumentLoaderTest.java
new file mode 100644
index 000000000000..49fcddd4b604
--- /dev/null
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/DocumentLoaderTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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 android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+@SmallTest
+public class DocumentLoaderTest extends AndroidTestCase {
+ private BlockableTestMtpMaanger mManager;
+ private TestContentResolver mResolver;
+ private DocumentLoader mLoader;
+ final private Identifier mParentIdentifier = new Identifier(0, 0, 0);
+
+ @Override
+ public void setUp() {
+ mManager = new BlockableTestMtpMaanger(getContext());
+ mResolver = new TestContentResolver();
+ mLoader = new DocumentLoader(mManager, mResolver);
+ }
+
+ public void testBasic() throws IOException, InterruptedException {
+ final Uri uri = DocumentsContract.buildChildDocumentsUri(
+ MtpDocumentsProvider.AUTHORITY, mParentIdentifier.toDocumentId());
+ setUpDocument(mManager, 40);
+ mManager.blockDocument(0, 15);
+ mManager.blockDocument(0, 35);
+
+ {
+ final Cursor cursor = mLoader.queryChildDocuments(
+ MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier);
+ assertEquals(DocumentLoader.NUM_INITIAL_ENTRIES, cursor.getCount());
+ }
+
+ Thread.sleep(DocumentLoader.NOTIFY_PERIOD_MS);
+ mManager.unblockDocument(0, 15);
+ mResolver.waitForNotification(uri, 1);
+
+ {
+ final Cursor cursor = mLoader.queryChildDocuments(
+ MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier);
+ assertEquals(
+ DocumentLoader.NUM_INITIAL_ENTRIES + DocumentLoader.NUM_LOADING_ENTRIES,
+ cursor.getCount());
+ }
+
+ mManager.unblockDocument(0, 35);
+ mResolver.waitForNotification(uri, 2);
+
+ {
+ final Cursor cursor = mLoader.queryChildDocuments(
+ MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION, mParentIdentifier);
+ assertEquals(40, cursor.getCount());
+ }
+
+ assertEquals(2, mResolver.getChangeCount(uri));
+ }
+
+ private void setUpDocument(TestMtpManager manager, int count) {
+ int[] childDocuments = new int[count];
+ for (int i = 0; i < childDocuments.length; i++) {
+ final int objectHandle = i + 1;
+ childDocuments[i] = objectHandle;
+ manager.setDocument(0, objectHandle, new MtpDocument(
+ objectHandle,
+ 0 /* format */,
+ "file" + objectHandle,
+ new Date(),
+ 1024,
+ 0 /* thumbnail size */));
+ }
+ manager.setObjectHandles(0, 0, MtpManager.OBJECT_HANDLE_ROOT_CHILDREN, childDocuments);
+ }
+
+ private static class BlockableTestMtpMaanger extends TestMtpManager {
+ final private Map<String, CountDownLatch> blockedDocuments = new HashMap<>();
+
+ BlockableTestMtpMaanger(Context context) {
+ super(context);
+ }
+
+ void blockDocument(int deviceId, int objectHandle) {
+ blockedDocuments.put(pack(deviceId, objectHandle), new CountDownLatch(1));
+ }
+
+ void unblockDocument(int deviceId, int objectHandle) {
+ blockedDocuments.get(pack(deviceId, objectHandle)).countDown();
+ }
+
+ @Override
+ MtpDocument getDocument(int deviceId, int objectHandle) throws IOException {
+ final CountDownLatch latch = blockedDocuments.get(pack(deviceId, objectHandle));
+ if (latch != null) {
+ try {
+ latch.await();
+ } catch(InterruptedException e) {
+ fail();
+ }
+ }
+ return super.getDocument(deviceId, objectHandle);
+ }
+ }
+}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
index 1031d8a7511f..e0e3ce643bb1 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
@@ -16,30 +16,26 @@
package com.android.mtp;
-import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Root;
import android.test.AndroidTestCase;
-import android.test.mock.MockContentResolver;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
@SmallTest
public class MtpDocumentsProviderTest extends AndroidTestCase {
- private ContentResolver mResolver;
+ private TestContentResolver mResolver;
private MtpDocumentsProvider mProvider;
private TestMtpManager mMtpManager;
@Override
public void setUp() {
- mResolver = new ContentResolver();
+ mResolver = new TestContentResolver();
mMtpManager = new TestMtpManager(getContext());
mProvider = new MtpDocumentsProvider();
mProvider.onCreateForTesting(mMtpManager, mResolver);
@@ -291,21 +287,4 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
DocumentsContract.buildChildDocumentsUri(
MtpDocumentsProvider.AUTHORITY, "0_0_2")));
}
-
- private static class ContentResolver extends MockContentResolver {
- final Map<Uri, Integer> mChangeCounts = new HashMap<Uri, Integer>();
-
- @Override
- public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
- mChangeCounts.put(uri, getChangeCount(uri) + 1);
- }
-
- int getChangeCount(Uri uri) {
- if (mChangeCounts.containsKey(uri)) {
- return mChangeCounts.get(uri);
- } else {
- return 0;
- }
- }
- }
}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestContentResolver.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestContentResolver.java
new file mode 100644
index 000000000000..33e559f6c1d2
--- /dev/null
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestContentResolver.java
@@ -0,0 +1,57 @@
+/*
+ * 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.database.ContentObserver;
+import android.net.Uri;
+import android.test.mock.MockContentResolver;
+
+import junit.framework.Assert;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Phaser;
+
+class TestContentResolver extends MockContentResolver {
+ final private Map<Uri, Phaser> mPhasers = new HashMap<>();
+
+ @Override
+ public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) {
+ getPhaser(uri).arrive();
+ }
+
+ void waitForNotification(Uri uri, int count) {
+ Assert.assertEquals(count, getPhaser(uri).awaitAdvance(count - 1));
+ }
+
+ int getChangeCount(Uri uri) {
+ if (mPhasers.containsKey(uri)) {
+ return mPhasers.get(uri).getPhase();
+ } else {
+ return 0;
+ }
+ }
+
+ private synchronized Phaser getPhaser(Uri uri) {
+ Phaser phaser = mPhasers.get(uri);
+ if (phaser == null) {
+ phaser = new Phaser(1);
+ mPhasers.put(uri, phaser);
+ }
+ return phaser;
+ }
+}
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java
index 7f8168664954..40de7b40923e 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java
@@ -28,7 +28,7 @@ import java.util.Set;
import java.util.TreeSet;
public class TestMtpManager extends MtpManager {
- private static String pack(int... args) {
+ protected static String pack(int... args) {
return Arrays.toString(args);
}