DocumentLoader.java revision 619afdaae1ec7dcbd71bb1f698a0901a1fa290fe
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.mtp; 18 19import android.content.ContentResolver; 20import android.database.Cursor; 21import android.mtp.MtpObjectInfo; 22import android.net.Uri; 23import android.os.Bundle; 24import android.os.Process; 25import android.provider.DocumentsContract; 26import android.util.Log; 27 28import java.io.FileNotFoundException; 29import java.io.IOException; 30import java.util.ArrayList; 31import java.util.Arrays; 32import java.util.Date; 33import java.util.LinkedList; 34 35/** 36 * Loader for MTP document. 37 * At the first request, the loader returns only first NUM_INITIAL_ENTRIES. Then it launches 38 * background thread to load the rest documents and caches its result for next requests. 39 * TODO: Rename this class to ObjectInfoLoader 40 */ 41class DocumentLoader { 42 static final int NUM_INITIAL_ENTRIES = 10; 43 static final int NUM_LOADING_ENTRIES = 20; 44 static final int NOTIFY_PERIOD_MS = 500; 45 46 private final MtpManager mMtpManager; 47 private final ContentResolver mResolver; 48 private final MtpDatabase mDatabase; 49 private final TaskList mTaskList = new TaskList(); 50 private boolean mHasBackgroundThread = false; 51 52 DocumentLoader(MtpManager mtpManager, ContentResolver resolver, MtpDatabase database) { 53 mMtpManager = mtpManager; 54 mResolver = resolver; 55 mDatabase = database; 56 } 57 58 private static MtpObjectInfo[] loadDocuments(MtpManager manager, int deviceId, int[] handles) 59 throws IOException { 60 final ArrayList<MtpObjectInfo> objects = new ArrayList<>(); 61 for (int i = 0; i < handles.length; i++) { 62 final MtpObjectInfo info = manager.getObjectInfo(deviceId, handles[i]); 63 if (info == null) { 64 Log.e(MtpDocumentsProvider.TAG, 65 "Failed to obtain object info handle=" + handles[i]); 66 continue; 67 } 68 objects.add(info); 69 } 70 return objects.toArray(new MtpObjectInfo[objects.size()]); 71 } 72 73 synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent) 74 throws IOException { 75 LoaderTask task = mTaskList.findTask(parent); 76 if (task == null) { 77 if (parent.mDocumentId == null) { 78 throw new FileNotFoundException("Parent not found."); 79 } 80 81 int parentHandle = parent.mObjectHandle; 82 // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to 83 // getObjectHandles if we would like to obtain children under the root. 84 if (parent.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) { 85 parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN; 86 } 87 // TODO: Handle nit race around here. 88 // 1. getObjectHandles. 89 // 2. putNewDocument. 90 // 3. startAddingChildDocuemnts. 91 // 4. stopAddingChildDocuments - It removes the new document added at the step 2, 92 // because it is not updated between start/stopAddingChildDocuments. 93 task = new LoaderTask(mDatabase, parent, mMtpManager.getObjectHandles( 94 parent.mDeviceId, parent.mStorageId, parentHandle)); 95 task.fillDocuments(loadDocuments( 96 mMtpManager, 97 parent.mDeviceId, 98 task.getUnloadedObjectHandles(NUM_INITIAL_ENTRIES))); 99 } else { 100 // Once remove the existing task in order to add it to the head of the list. 101 mTaskList.remove(task); 102 } 103 104 mTaskList.addFirst(task); 105 if (task.getState() == LoaderTask.STATE_LOADING && !mHasBackgroundThread) { 106 mHasBackgroundThread = true; 107 new BackgroundLoaderThread().start(); 108 } 109 return task.createCursor(mResolver, columnNames); 110 } 111 112 synchronized void clearTasks() { 113 mTaskList.clear(); 114 } 115 116 synchronized void clearCompletedTasks() { 117 mTaskList.clearCompletedTasks(); 118 } 119 120 synchronized void clearTask(Identifier parentIdentifier) { 121 mTaskList.clearTask(parentIdentifier); 122 } 123 124 private class BackgroundLoaderThread extends Thread { 125 @Override 126 public void run() { 127 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 128 while (true) { 129 LoaderTask task; 130 int deviceId; 131 int[] handles; 132 synchronized (DocumentLoader.this) { 133 task = mTaskList.findRunningTask(); 134 if (task == null) { 135 mHasBackgroundThread = false; 136 return; 137 } 138 deviceId = task.mIdentifier.mDeviceId; 139 handles = task.getUnloadedObjectHandles(NUM_LOADING_ENTRIES); 140 } 141 142 try { 143 final MtpObjectInfo[] objectInfos = 144 loadDocuments(mMtpManager, deviceId, handles); 145 task.fillDocuments(objectInfos); 146 final boolean shouldNotify = 147 task.mLastNotified.getTime() < 148 new Date().getTime() - NOTIFY_PERIOD_MS || 149 task.getState() != LoaderTask.STATE_LOADING; 150 if (shouldNotify) { 151 task.notify(mResolver); 152 } 153 } catch (IOException exception) { 154 task.setError(exception); 155 } 156 } 157 } 158 } 159 160 private static class TaskList extends LinkedList<LoaderTask> { 161 LoaderTask findTask(Identifier parent) { 162 for (int i = 0; i < size(); i++) { 163 if (get(i).mIdentifier.equals(parent)) 164 return get(i); 165 } 166 return null; 167 } 168 169 LoaderTask findRunningTask() { 170 for (int i = 0; i < size(); i++) { 171 if (get(i).getState() == LoaderTask.STATE_LOADING) 172 return get(i); 173 } 174 return null; 175 } 176 177 void clearCompletedTasks() { 178 int i = 0; 179 while (i < size()) { 180 if (get(i).getState() == LoaderTask.STATE_COMPLETED) { 181 remove(i); 182 } else { 183 i++; 184 } 185 } 186 } 187 188 void clearTask(Identifier parentIdentifier) { 189 for (int i = 0; i < size(); i++) { 190 final LoaderTask task = get(i); 191 if (task.mIdentifier.mDeviceId == parentIdentifier.mDeviceId && 192 task.mIdentifier.mObjectHandle == parentIdentifier.mObjectHandle) { 193 remove(i); 194 return; 195 } 196 } 197 } 198 } 199 200 private static class LoaderTask { 201 static final int STATE_LOADING = 0; 202 static final int STATE_COMPLETED = 1; 203 static final int STATE_ERROR = 2; 204 205 final MtpDatabase mDatabase; 206 final Identifier mIdentifier; 207 final int[] mObjectHandles; 208 Date mLastNotified; 209 int mNumLoaded; 210 Exception mError; 211 212 LoaderTask(MtpDatabase database, Identifier identifier, int[] objectHandles) { 213 mDatabase = database; 214 mIdentifier = identifier; 215 mObjectHandles = objectHandles; 216 mNumLoaded = 0; 217 mLastNotified = new Date(); 218 } 219 220 Cursor createCursor(ContentResolver resolver, String[] columnNames) throws IOException { 221 final Bundle extras = new Bundle(); 222 switch (getState()) { 223 case STATE_LOADING: 224 extras.putBoolean(DocumentsContract.EXTRA_LOADING, true); 225 break; 226 case STATE_ERROR: 227 throw new IOException(mError); 228 } 229 230 final Cursor cursor = 231 mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId); 232 cursor.setNotificationUri(resolver, createUri()); 233 cursor.respond(extras); 234 235 return cursor; 236 } 237 238 int getState() { 239 if (mError != null) { 240 return STATE_ERROR; 241 } else if (mNumLoaded == mObjectHandles.length) { 242 return STATE_COMPLETED; 243 } else { 244 return STATE_LOADING; 245 } 246 } 247 248 int[] getUnloadedObjectHandles(int count) { 249 return Arrays.copyOfRange( 250 mObjectHandles, 251 mNumLoaded, 252 Math.min(mNumLoaded + count, mObjectHandles.length)); 253 } 254 255 void notify(ContentResolver resolver) { 256 resolver.notifyChange(createUri(), null, false); 257 mLastNotified = new Date(); 258 } 259 260 void fillDocuments(MtpObjectInfo[] objectInfoList) { 261 if (objectInfoList.length == 0 || getState() != STATE_LOADING) { 262 return; 263 } 264 try{ 265 if (mNumLoaded == 0) { 266 mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId); 267 } 268 mDatabase.getMapper().putChildDocuments( 269 mIdentifier.mDeviceId, mIdentifier.mDocumentId, objectInfoList); 270 mNumLoaded += objectInfoList.length; 271 if (getState() != STATE_LOADING) { 272 mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId); 273 } 274 } catch (FileNotFoundException exception) { 275 setErrorInternal(exception); 276 } 277 } 278 279 void setError(Exception error) { 280 final int lastState = getState(); 281 setErrorInternal(error); 282 if (lastState == STATE_LOADING) { 283 try { 284 mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId); 285 } catch (FileNotFoundException exception) { 286 setErrorInternal(exception); 287 } 288 } 289 } 290 291 private void setErrorInternal(Exception error) { 292 Log.e(MtpDocumentsProvider.TAG, "Error in DocumentLoader thread", error); 293 mError = error; 294 mNumLoaded = 0; 295 } 296 297 private Uri createUri() { 298 return DocumentsContract.buildChildDocumentsUri( 299 MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId); 300 } 301 } 302} 303