| /* |
| * 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); |
| } |
| } |
| } |