blob: 712dbcb3463da437e44fc182651642a7ea18f5a1 [file] [log] [blame]
/*
* 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.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.Arrays;
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 int mDeviceId;
private final MtpManager mMtpManager;
private final ContentResolver mResolver;
private final MtpDatabase mDatabase;
private final TaskList mTaskList = new TaskList();
private Thread mBackgroundThread;
DocumentLoader(int deviceId, MtpManager mtpManager, ContentResolver resolver,
MtpDatabase database) {
mDeviceId = deviceId;
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 {
Preconditions.checkArgument(parent.mDeviceId == mDeviceId);
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 = LoaderTask.create(mDatabase, mMtpManager, parent);
task.fillDocuments(loadDocuments(
mMtpManager,
parent.mDeviceId,
task.getUnloadedObjectHandles(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);
final LoaderTask task = mTaskList.findRunningTask();
if (task != null) {
return task;
}
final Identifier identifier = mDatabase.getUnmappedDocumentsParent(mDeviceId);
if (identifier != null) {
final LoaderTask existingTask = mTaskList.findTask(identifier);
if (existingTask != null) {
Preconditions.checkState(existingTask.getState() != LoaderTask.STATE_LOADING);
mTaskList.remove(existingTask);
}
try {
final LoaderTask newTask = LoaderTask.create(mDatabase, mMtpManager, identifier);
mTaskList.addFirst(newTask);
return newTask;
} catch (IOException exception) {
Log.e(MtpDocumentsProvider.TAG, "Failed to create a task for mapping", exception);
// Continue to release the background thread.
}
}
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();
}
synchronized void clearTask(Identifier parentIdentifier) {
mTaskList.clearTask(parentIdentifier);
}
/**
* Helper method to loads multiple object info.
*/
private static MtpObjectInfo[] loadDocuments(MtpManager manager, int deviceId, int[] handles)
throws IOException {
final ArrayList<MtpObjectInfo> objects = new ArrayList<>();
for (int i = 0; i < handles.length; i++) {
final MtpObjectInfo info = manager.getObjectInfo(deviceId, handles[i]);
if (info == null) {
Log.e(MtpDocumentsProvider.TAG,
"Failed to obtain object info handle=" + handles[i]);
continue;
}
objects.add(info);
}
return objects.toArray(new MtpObjectInfo[objects.size()]);
}
/**
* 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;
}
try {
final MtpObjectInfo[] objectInfos = loadDocuments(
mMtpManager,
task.mIdentifier.mDeviceId,
task.getUnloadedObjectHandles(NUM_LOADING_ENTRIES));
task.fillDocuments(objectInfos);
final boolean shouldNotify =
task.mLastNotified.getTime() <
new Date().getTime() - NOTIFY_PERIOD_MS ||
task.getState() != LoaderTask.STATE_LOADING;
if (shouldNotify) {
task.notify(mResolver);
}
} catch (IOException exception) {
task.setError(exception);
}
}
}
}
/**
* 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;
}
LoaderTask findRunningTask() {
for (int i = 0; i < size(); i++) {
if (get(i).getState() == LoaderTask.STATE_LOADING)
return get(i);
}
return null;
}
void clearCompletedTasks() {
int i = 0;
while (i < size()) {
if (get(i).getState() == LoaderTask.STATE_COMPLETED) {
remove(i);
} else {
i++;
}
}
}
void clearTask(Identifier parentIdentifier) {
for (int i = 0; i < size(); i++) {
final LoaderTask task = get(i);
if (task.mIdentifier.mDeviceId == parentIdentifier.mDeviceId &&
task.mIdentifier.mObjectHandle == parentIdentifier.mObjectHandle) {
remove(i);
return;
}
}
}
}
/**
* Loader task.
* Each task is responsible for fetching child documents for the given parent document.
*/
private static class LoaderTask {
static final int STATE_LOADING = 0;
static final int STATE_COMPLETED = 1;
static final int STATE_ERROR = 2;
final MtpDatabase mDatabase;
final Identifier mIdentifier;
final int[] mObjectHandles;
Date mLastNotified;
int mNumLoaded;
Exception mError;
LoaderTask(MtpDatabase database, Identifier identifier, int[] objectHandles) {
mDatabase = database;
mIdentifier = identifier;
mObjectHandles = objectHandles;
mNumLoaded = 0;
mLastNotified = new Date();
}
/**
* 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.
*/
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 new IOException(mError);
}
final Cursor cursor =
mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId);
cursor.setNotificationUri(resolver, createUri());
cursor.respond(extras);
return cursor;
}
/**
* Returns a state of the task.
*/
int getState() {
if (mError != null) {
return STATE_ERROR;
} else if (mNumLoaded == mObjectHandles.length) {
return STATE_COMPLETED;
} else {
return STATE_LOADING;
}
}
/**
* Obtains object handles that have not been loaded yet.
*/
int[] getUnloadedObjectHandles(int count) {
return Arrays.copyOfRange(
mObjectHandles,
mNumLoaded,
Math.min(mNumLoaded + count, mObjectHandles.length));
}
/**
* Notifies a change of child list of the document.
*/
void notify(ContentResolver resolver) {
resolver.notifyChange(createUri(), null, false);
mLastNotified = new Date();
}
/**
* Stores object information into database.
*/
void fillDocuments(MtpObjectInfo[] objectInfoList) {
if (objectInfoList.length == 0 || getState() != STATE_LOADING) {
return;
}
try{
if (mNumLoaded == 0) {
mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId);
}
mDatabase.getMapper().putChildDocuments(
mIdentifier.mDeviceId, mIdentifier.mDocumentId, objectInfoList);
mNumLoaded += objectInfoList.length;
if (getState() != STATE_LOADING) {
mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId);
}
} catch (FileNotFoundException exception) {
setErrorInternal(exception);
}
}
/**
* Marks the loading task as error.
*/
void setError(Exception error) {
final int lastState = getState();
setErrorInternal(error);
if (lastState == STATE_LOADING) {
try {
mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId);
} catch (FileNotFoundException exception) {
setErrorInternal(exception);
}
}
}
private void setErrorInternal(Exception error) {
Log.e(MtpDocumentsProvider.TAG, "Error in DocumentLoader thread", error);
mError = error;
mNumLoaded = 0;
}
private Uri createUri() {
return DocumentsContract.buildChildDocumentsUri(
MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId);
}
/**
* Creates a LoaderTask that loads children of the given document.
*/
static LoaderTask create(MtpDatabase database, MtpManager manager, Identifier parent)
throws IOException {
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 (parent.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
}
return new LoaderTask(database, parent, manager.getObjectHandles(
parent.mDeviceId, parent.mStorageId, parentHandle));
}
}
}