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