Loading packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java 0 → 100644 +222 −0 Original line number Diff line number Diff line /* * 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()); } } } packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java +15 −28 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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. Loading @@ -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 Loading Loading @@ -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 { Loading @@ -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()); } Loading Loading @@ -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(); Loading @@ -241,6 +226,7 @@ public class MtpDocumentsProvider extends DocumentsProvider { void closeDevice(int deviceId) throws IOException { mMtpManager.closeDevice(deviceId); mDocumentLoader.clearCache(deviceId); notifyRootsChange(); } Loading @@ -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); Loading packages/MtpDocumentsProvider/tests/src/com/android/mtp/DocumentLoaderTest.java 0 → 100644 +127 −0 Original line number Diff line number Diff line /* * 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); } } } packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java +2 −23 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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; } } } } packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestContentResolver.java 0 → 100644 +57 −0 Original line number Diff line number Diff line /* * 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; } } Loading
packages/MtpDocumentsProvider/src/com/android/mtp/DocumentLoader.java 0 → 100644 +222 −0 Original line number Diff line number Diff line /* * 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()); } } }
packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java +15 −28 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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. Loading @@ -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 Loading Loading @@ -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 { Loading @@ -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()); } Loading Loading @@ -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(); Loading @@ -241,6 +226,7 @@ public class MtpDocumentsProvider extends DocumentsProvider { void closeDevice(int deviceId) throws IOException { mMtpManager.closeDevice(deviceId); mDocumentLoader.clearCache(deviceId); notifyRootsChange(); } Loading @@ -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); Loading
packages/MtpDocumentsProvider/tests/src/com/android/mtp/DocumentLoaderTest.java 0 → 100644 +127 −0 Original line number Diff line number Diff line /* * 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); } } }
packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java +2 −23 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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; } } } }
packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestContentResolver.java 0 → 100644 +57 −0 Original line number Diff line number Diff line /* * 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; } }