Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6baa16e9 authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Let MTPDocumentsProvider load documents in background thread.

The CL introduce DocumentLoader class that has a responsibility for loading
documents on background thread. When the provider is requested documents and the
result is not cached, it returns the cursor containing the first 10 items with
extra loading flag. Then it loads the rest of documents into cache on background
thread and notifies update regularly.

BUG=23067619

Change-Id: I7ac9d919bc3ffee1960ae3e8e7272a792a982ea8
parent 6eed9a23
Loading
Loading
Loading
Loading
+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());
        }
    }
}
+15 −28
Original line number Diff line number Diff line
@@ -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);
+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);
        }
    }
}
+2 −23
Original line number Diff line number Diff line
@@ -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;
            }
        }
    }
}
+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