|  | /* | 
|  | * 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.annotation.Nullable; | 
|  | import android.annotation.WorkerThread; | 
|  | import android.content.ContentResolver; | 
|  | import android.database.Cursor; | 
|  | import android.mtp.MtpConstants; | 
|  | import android.mtp.MtpObjectInfo; | 
|  | import android.net.Uri; | 
|  | import android.os.Bundle; | 
|  | import android.os.Process; | 
|  | import android.provider.DocumentsContract; | 
|  | import android.util.Log; | 
|  |  | 
|  | import com.android.internal.util.Preconditions; | 
|  |  | 
|  | import java.io.FileNotFoundException; | 
|  | import java.io.IOException; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Date; | 
|  | import java.util.LinkedList; | 
|  |  | 
|  | /** | 
|  | * Loader for MTP document. | 
|  | * At the first request, the loader returns only first NUM_INITIAL_ENTRIES. Then it launches | 
|  | * background thread to load the rest documents and caches its result for next requests. | 
|  | * TODO: Rename this class to ObjectInfoLoader | 
|  | */ | 
|  | class DocumentLoader implements AutoCloseable { | 
|  | static final int NUM_INITIAL_ENTRIES = 10; | 
|  | static final int NUM_LOADING_ENTRIES = 20; | 
|  | static final int NOTIFY_PERIOD_MS = 500; | 
|  |  | 
|  | private final MtpDeviceRecord mDevice; | 
|  | private final MtpManager mMtpManager; | 
|  | private final ContentResolver mResolver; | 
|  | private final MtpDatabase mDatabase; | 
|  | private final TaskList mTaskList = new TaskList(); | 
|  | private Thread mBackgroundThread; | 
|  |  | 
|  | DocumentLoader(MtpDeviceRecord device, MtpManager mtpManager, ContentResolver resolver, | 
|  | MtpDatabase database) { | 
|  | mDevice = device; | 
|  | mMtpManager = mtpManager; | 
|  | mResolver = resolver; | 
|  | mDatabase = database; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Queries the child documents of given parent. | 
|  | * It loads the first NUM_INITIAL_ENTRIES of object info, then launches the background thread | 
|  | * to load the rest. | 
|  | */ | 
|  | synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent) | 
|  | throws IOException { | 
|  | assert parent.mDeviceId == mDevice.deviceId; | 
|  |  | 
|  | LoaderTask task = mTaskList.findTask(parent); | 
|  | if (task == null) { | 
|  | if (parent.mDocumentId == null) { | 
|  | throw new FileNotFoundException("Parent not found."); | 
|  | } | 
|  | // TODO: Handle nit race around here. | 
|  | // 1. getObjectHandles. | 
|  | // 2. putNewDocument. | 
|  | // 3. startAddingChildDocuemnts. | 
|  | // 4. stopAddingChildDocuments - It removes the new document added at the step 2, | 
|  | //     because it is not updated between start/stopAddingChildDocuments. | 
|  | task = new LoaderTask(mMtpManager, mDatabase, mDevice.operationsSupported, parent); | 
|  | task.loadObjectHandles(); | 
|  | task.loadObjectInfoList(NUM_INITIAL_ENTRIES); | 
|  | } else { | 
|  | // Once remove the existing task in order to add it to the head of the list. | 
|  | mTaskList.remove(task); | 
|  | } | 
|  |  | 
|  | mTaskList.addFirst(task); | 
|  | if (task.getState() == LoaderTask.STATE_LOADING) { | 
|  | resume(); | 
|  | } | 
|  | return task.createCursor(mResolver, columnNames); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Resumes a background thread. | 
|  | */ | 
|  | synchronized void resume() { | 
|  | if (mBackgroundThread == null) { | 
|  | mBackgroundThread = new BackgroundLoaderThread(); | 
|  | mBackgroundThread.start(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Obtains next task to be run in background thread, or release the reference to background | 
|  | * thread. | 
|  | * | 
|  | * Worker thread that receives null task needs to exit. | 
|  | */ | 
|  | @WorkerThread | 
|  | synchronized @Nullable LoaderTask getNextTaskOrReleaseBackgroundThread() { | 
|  | Preconditions.checkState(mBackgroundThread != null); | 
|  |  | 
|  | for (final LoaderTask task : mTaskList) { | 
|  | if (task.getState() == LoaderTask.STATE_LOADING) { | 
|  | return task; | 
|  | } | 
|  | } | 
|  |  | 
|  | final Identifier identifier = mDatabase.getUnmappedDocumentsParent(mDevice.deviceId); | 
|  | if (identifier != null) { | 
|  | final LoaderTask existingTask = mTaskList.findTask(identifier); | 
|  | if (existingTask != null) { | 
|  | Preconditions.checkState(existingTask.getState() != LoaderTask.STATE_LOADING); | 
|  | mTaskList.remove(existingTask); | 
|  | } | 
|  | final LoaderTask newTask = new LoaderTask( | 
|  | mMtpManager, mDatabase, mDevice.operationsSupported, identifier); | 
|  | newTask.loadObjectHandles(); | 
|  | mTaskList.addFirst(newTask); | 
|  | return newTask; | 
|  | } | 
|  |  | 
|  | mBackgroundThread = null; | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Terminates background thread. | 
|  | */ | 
|  | @Override | 
|  | public void close() throws InterruptedException { | 
|  | final Thread thread; | 
|  | synchronized (this) { | 
|  | mTaskList.clear(); | 
|  | thread = mBackgroundThread; | 
|  | } | 
|  | if (thread != null) { | 
|  | thread.interrupt(); | 
|  | thread.join(); | 
|  | } | 
|  | } | 
|  |  | 
|  | synchronized void clearCompletedTasks() { | 
|  | mTaskList.clearCompletedTasks(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Cancels the task for |parentIdentifier|. | 
|  | * | 
|  | * Task is removed from the cached list and it will create new task when |parentIdentifier|'s | 
|  | * children are queried next. | 
|  | */ | 
|  | void cancelTask(Identifier parentIdentifier) { | 
|  | final LoaderTask task; | 
|  | synchronized (this) { | 
|  | task = mTaskList.findTask(parentIdentifier); | 
|  | } | 
|  | if (task != null) { | 
|  | task.cancel(); | 
|  | mTaskList.remove(task); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Background thread to fetch object info. | 
|  | */ | 
|  | private class BackgroundLoaderThread extends Thread { | 
|  | /** | 
|  | * Finds task that needs to be processed, then loads NUM_LOADING_ENTRIES of object info and | 
|  | * store them to the database. If it does not find a task, exits the thread. | 
|  | */ | 
|  | @Override | 
|  | public void run() { | 
|  | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); | 
|  | while (!Thread.interrupted()) { | 
|  | final LoaderTask task = getNextTaskOrReleaseBackgroundThread(); | 
|  | if (task == null) { | 
|  | return; | 
|  | } | 
|  | task.loadObjectInfoList(NUM_LOADING_ENTRIES); | 
|  | final boolean shouldNotify = | 
|  | task.getState() != LoaderTask.STATE_CANCELLED && | 
|  | (task.mLastNotified.getTime() < | 
|  | new Date().getTime() - NOTIFY_PERIOD_MS || | 
|  | task.getState() != LoaderTask.STATE_LOADING); | 
|  | if (shouldNotify) { | 
|  | task.notify(mResolver); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Task list that has helper methods to search/clear tasks. | 
|  | */ | 
|  | private static class TaskList extends LinkedList<LoaderTask> { | 
|  | LoaderTask findTask(Identifier parent) { | 
|  | for (int i = 0; i < size(); i++) { | 
|  | if (get(i).mIdentifier.equals(parent)) | 
|  | return get(i); | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | void clearCompletedTasks() { | 
|  | int i = 0; | 
|  | while (i < size()) { | 
|  | if (get(i).getState() == LoaderTask.STATE_COMPLETED) { | 
|  | remove(i); | 
|  | } else { | 
|  | i++; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Loader task. | 
|  | * Each task is responsible for fetching child documents for the given parent document. | 
|  | */ | 
|  | private static class LoaderTask { | 
|  | static final int STATE_START = 0; | 
|  | static final int STATE_LOADING = 1; | 
|  | static final int STATE_COMPLETED = 2; | 
|  | static final int STATE_ERROR = 3; | 
|  | static final int STATE_CANCELLED = 4; | 
|  |  | 
|  | final MtpManager mManager; | 
|  | final MtpDatabase mDatabase; | 
|  | final int[] mOperationsSupported; | 
|  | final Identifier mIdentifier; | 
|  | int[] mObjectHandles; | 
|  | int mState; | 
|  | Date mLastNotified; | 
|  | int mPosition; | 
|  | IOException mError; | 
|  |  | 
|  | LoaderTask(MtpManager manager, MtpDatabase database, int[] operationsSupported, | 
|  | Identifier identifier) { | 
|  | assert operationsSupported != null; | 
|  | assert identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE; | 
|  | mManager = manager; | 
|  | mDatabase = database; | 
|  | mOperationsSupported = operationsSupported; | 
|  | mIdentifier = identifier; | 
|  | mObjectHandles = null; | 
|  | mState = STATE_START; | 
|  | mPosition = 0; | 
|  | mLastNotified = new Date(); | 
|  | } | 
|  |  | 
|  | synchronized void loadObjectHandles() { | 
|  | assert mState == STATE_START; | 
|  | mPosition = 0; | 
|  | int parentHandle = mIdentifier.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 (mIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) { | 
|  | parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN; | 
|  | } | 
|  | try { | 
|  | mObjectHandles = mManager.getObjectHandles( | 
|  | mIdentifier.mDeviceId, mIdentifier.mStorageId, parentHandle); | 
|  | mState = STATE_LOADING; | 
|  | } catch (IOException error) { | 
|  | mError = error; | 
|  | mState = STATE_ERROR; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a cursor that traverses the child document of the parent document handled by the | 
|  | * task. | 
|  | * The returned task may have a EXTRA_LOADING flag. | 
|  | */ | 
|  | synchronized Cursor createCursor(ContentResolver resolver, String[] columnNames) | 
|  | throws IOException { | 
|  | final Bundle extras = new Bundle(); | 
|  | switch (getState()) { | 
|  | case STATE_LOADING: | 
|  | extras.putBoolean(DocumentsContract.EXTRA_LOADING, true); | 
|  | break; | 
|  | case STATE_ERROR: | 
|  | throw mError; | 
|  | } | 
|  | final Cursor cursor = | 
|  | mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId); | 
|  | cursor.setExtras(extras); | 
|  | cursor.setNotificationUri(resolver, createUri()); | 
|  | return cursor; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Stores object information into database. | 
|  | */ | 
|  | void loadObjectInfoList(int count) { | 
|  | synchronized (this) { | 
|  | if (mState != STATE_LOADING) { | 
|  | return; | 
|  | } | 
|  | if (mPosition == 0) { | 
|  | try{ | 
|  | mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId); | 
|  | } catch (FileNotFoundException error) { | 
|  | mError = error; | 
|  | mState = STATE_ERROR; | 
|  | return; | 
|  | } | 
|  | } | 
|  | } | 
|  | final ArrayList<MtpObjectInfo> infoList = new ArrayList<>(); | 
|  | for (int chunkEnd = mPosition + count; | 
|  | mPosition < mObjectHandles.length && mPosition < chunkEnd; | 
|  | mPosition++) { | 
|  | try { | 
|  | infoList.add(mManager.getObjectInfo( | 
|  | mIdentifier.mDeviceId, mObjectHandles[mPosition])); | 
|  | } catch (IOException error) { | 
|  | Log.e(MtpDocumentsProvider.TAG, "Failed to load object info", error); | 
|  | } | 
|  | } | 
|  | final long[] objectSizeList = new long[infoList.size()]; | 
|  | for (int i = 0; i < infoList.size(); i++) { | 
|  | final MtpObjectInfo info = infoList.get(i); | 
|  | // Compressed size is 32-bit unsigned integer but getCompressedSize returns the | 
|  | // value in Java int (signed 32-bit integer). Use getCompressedSizeLong instead | 
|  | // to get the value in Java long. | 
|  | if (info.getCompressedSizeLong() != 0xffffffffl) { | 
|  | objectSizeList[i] = info.getCompressedSizeLong(); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | if (!MtpDeviceRecord.isSupported( | 
|  | mOperationsSupported, | 
|  | MtpConstants.OPERATION_GET_OBJECT_PROP_DESC) || | 
|  | !MtpDeviceRecord.isSupported( | 
|  | mOperationsSupported, | 
|  | MtpConstants.OPERATION_GET_OBJECT_PROP_VALUE)) { | 
|  | objectSizeList[i] = -1; | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Object size is more than 4GB. | 
|  | try { | 
|  | objectSizeList[i] = mManager.getObjectSizeLong( | 
|  | mIdentifier.mDeviceId, | 
|  | info.getObjectHandle(), | 
|  | info.getFormat()); | 
|  | } catch (IOException error) { | 
|  | Log.e(MtpDocumentsProvider.TAG, "Failed to get object size property.", error); | 
|  | objectSizeList[i] = -1; | 
|  | } | 
|  | } | 
|  | synchronized (this) { | 
|  | // Check if the task is cancelled or not. | 
|  | if (mState != STATE_LOADING) { | 
|  | return; | 
|  | } | 
|  | try { | 
|  | mDatabase.getMapper().putChildDocuments( | 
|  | mIdentifier.mDeviceId, | 
|  | mIdentifier.mDocumentId, | 
|  | mOperationsSupported, | 
|  | infoList.toArray(new MtpObjectInfo[infoList.size()]), | 
|  | objectSizeList); | 
|  | } catch (FileNotFoundException error) { | 
|  | // Looks like the parent document information is removed. | 
|  | // Adding documents has already cancelled in Mapper so we don't need to invoke | 
|  | // stopAddingDocuments. | 
|  | mError = error; | 
|  | mState = STATE_ERROR; | 
|  | return; | 
|  | } | 
|  | if (mPosition >= mObjectHandles.length) { | 
|  | try{ | 
|  | mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId); | 
|  | mState = STATE_COMPLETED; | 
|  | } catch (FileNotFoundException error) { | 
|  | mError = error; | 
|  | mState = STATE_ERROR; | 
|  | return; | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Cancels the task. | 
|  | */ | 
|  | synchronized void cancel() { | 
|  | mDatabase.getMapper().cancelAddingDocuments(mIdentifier.mDocumentId); | 
|  | mState = STATE_CANCELLED; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a state of the task. | 
|  | */ | 
|  | int getState() { | 
|  | return mState; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Notifies a change of child list of the document. | 
|  | */ | 
|  | void notify(ContentResolver resolver) { | 
|  | resolver.notifyChange(createUri(), null, false); | 
|  | mLastNotified = new Date(); | 
|  | } | 
|  |  | 
|  | private Uri createUri() { | 
|  | return DocumentsContract.buildChildDocumentsUri( | 
|  | MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId); | 
|  | } | 
|  | } | 
|  | } |