diff options
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); } |