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 17 package com.android.mtp; 18 19 import android.annotation.Nullable; 20 import android.annotation.WorkerThread; 21 import android.content.ContentResolver; 22 import android.database.Cursor; 23 import android.mtp.MtpConstants; 24 import android.mtp.MtpObjectInfo; 25 import android.net.Uri; 26 import android.os.Bundle; 27 import android.os.Process; 28 import android.provider.DocumentsContract; 29 import android.util.Log; 30 31 import com.android.internal.util.Preconditions; 32 33 import java.io.FileNotFoundException; 34 import java.io.IOException; 35 import java.util.ArrayList; 36 import java.util.Date; 37 import java.util.LinkedList; 38 39 /** 40 * Loader for MTP document. 41 * At the first request, the loader returns only first NUM_INITIAL_ENTRIES. Then it launches 42 * background thread to load the rest documents and caches its result for next requests. 43 * TODO: Rename this class to ObjectInfoLoader 44 */ 45 class DocumentLoader implements AutoCloseable { 46 static final int NUM_INITIAL_ENTRIES = 10; 47 static final int NUM_LOADING_ENTRIES = 20; 48 static final int NOTIFY_PERIOD_MS = 500; 49 50 private final MtpDeviceRecord mDevice; 51 private final MtpManager mMtpManager; 52 private final ContentResolver mResolver; 53 private final MtpDatabase mDatabase; 54 private final TaskList mTaskList = new TaskList(); 55 private Thread mBackgroundThread; 56 DocumentLoader(MtpDeviceRecord device, MtpManager mtpManager, ContentResolver resolver, MtpDatabase database)57 DocumentLoader(MtpDeviceRecord device, MtpManager mtpManager, ContentResolver resolver, 58 MtpDatabase database) { 59 mDevice = device; 60 mMtpManager = mtpManager; 61 mResolver = resolver; 62 mDatabase = database; 63 } 64 65 /** 66 * Queries the child documents of given parent. 67 * It loads the first NUM_INITIAL_ENTRIES of object info, then launches the background thread 68 * to load the rest. 69 */ queryChildDocuments(String[] columnNames, Identifier parent)70 synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent) 71 throws IOException { 72 assert parent.mDeviceId == mDevice.deviceId; 73 74 LoaderTask task = mTaskList.findTask(parent); 75 if (task == null) { 76 if (parent.mDocumentId == null) { 77 throw new FileNotFoundException("Parent not found."); 78 } 79 // TODO: Handle nit race around here. 80 // 1. getObjectHandles. 81 // 2. putNewDocument. 82 // 3. startAddingChildDocuemnts. 83 // 4. stopAddingChildDocuments - It removes the new document added at the step 2, 84 // because it is not updated between start/stopAddingChildDocuments. 85 task = new LoaderTask(mMtpManager, mDatabase, mDevice.operationsSupported, parent); 86 task.loadObjectHandles(); 87 task.loadObjectInfoList(NUM_INITIAL_ENTRIES); 88 } else { 89 // Once remove the existing task in order to add it to the head of the list. 90 mTaskList.remove(task); 91 } 92 93 mTaskList.addFirst(task); 94 if (task.getState() == LoaderTask.STATE_LOADING) { 95 resume(); 96 } 97 return task.createCursor(mResolver, columnNames); 98 } 99 100 /** 101 * Resumes a background thread. 102 */ resume()103 synchronized void resume() { 104 if (mBackgroundThread == null) { 105 mBackgroundThread = new BackgroundLoaderThread(); 106 mBackgroundThread.start(); 107 } 108 } 109 110 /** 111 * Obtains next task to be run in background thread, or release the reference to background 112 * thread. 113 * 114 * Worker thread that receives null task needs to exit. 115 */ 116 @WorkerThread getNextTaskOrReleaseBackgroundThread()117 synchronized @Nullable LoaderTask getNextTaskOrReleaseBackgroundThread() { 118 Preconditions.checkState(mBackgroundThread != null); 119 120 for (final LoaderTask task : mTaskList) { 121 if (task.getState() == LoaderTask.STATE_LOADING) { 122 return task; 123 } 124 } 125 126 final Identifier identifier = mDatabase.getUnmappedDocumentsParent(mDevice.deviceId); 127 if (identifier != null) { 128 final LoaderTask existingTask = mTaskList.findTask(identifier); 129 if (existingTask != null) { 130 Preconditions.checkState(existingTask.getState() != LoaderTask.STATE_LOADING); 131 mTaskList.remove(existingTask); 132 } 133 final LoaderTask newTask = new LoaderTask( 134 mMtpManager, mDatabase, mDevice.operationsSupported, identifier); 135 newTask.loadObjectHandles(); 136 mTaskList.addFirst(newTask); 137 return newTask; 138 } 139 140 mBackgroundThread = null; 141 return null; 142 } 143 144 /** 145 * Terminates background thread. 146 */ 147 @Override close()148 public void close() throws InterruptedException { 149 final Thread thread; 150 synchronized (this) { 151 mTaskList.clear(); 152 thread = mBackgroundThread; 153 } 154 if (thread != null) { 155 thread.interrupt(); 156 thread.join(); 157 } 158 } 159 clearCompletedTasks()160 synchronized void clearCompletedTasks() { 161 mTaskList.clearCompletedTasks(); 162 } 163 164 /** 165 * Cancels the task for |parentIdentifier|. 166 * 167 * Task is removed from the cached list and it will create new task when |parentIdentifier|'s 168 * children are queried next. 169 */ cancelTask(Identifier parentIdentifier)170 void cancelTask(Identifier parentIdentifier) { 171 final LoaderTask task; 172 synchronized (this) { 173 task = mTaskList.findTask(parentIdentifier); 174 } 175 if (task != null) { 176 task.cancel(); 177 mTaskList.remove(task); 178 } 179 } 180 181 /** 182 * Background thread to fetch object info. 183 */ 184 private class BackgroundLoaderThread extends Thread { 185 /** 186 * Finds task that needs to be processed, then loads NUM_LOADING_ENTRIES of object info and 187 * store them to the database. If it does not find a task, exits the thread. 188 */ 189 @Override run()190 public void run() { 191 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 192 while (!Thread.interrupted()) { 193 final LoaderTask task = getNextTaskOrReleaseBackgroundThread(); 194 if (task == null) { 195 return; 196 } 197 task.loadObjectInfoList(NUM_LOADING_ENTRIES); 198 final boolean shouldNotify = 199 task.getState() != LoaderTask.STATE_CANCELLED && 200 (task.mLastNotified.getTime() < 201 new Date().getTime() - NOTIFY_PERIOD_MS || 202 task.getState() != LoaderTask.STATE_LOADING); 203 if (shouldNotify) { 204 task.notify(mResolver); 205 } 206 } 207 } 208 } 209 210 /** 211 * Task list that has helper methods to search/clear tasks. 212 */ 213 private static class TaskList extends LinkedList<LoaderTask> { findTask(Identifier parent)214 LoaderTask findTask(Identifier parent) { 215 for (int i = 0; i < size(); i++) { 216 if (get(i).mIdentifier.equals(parent)) 217 return get(i); 218 } 219 return null; 220 } 221 clearCompletedTasks()222 void clearCompletedTasks() { 223 int i = 0; 224 while (i < size()) { 225 if (get(i).getState() == LoaderTask.STATE_COMPLETED) { 226 remove(i); 227 } else { 228 i++; 229 } 230 } 231 } 232 } 233 234 /** 235 * Loader task. 236 * Each task is responsible for fetching child documents for the given parent document. 237 */ 238 private static class LoaderTask { 239 static final int STATE_START = 0; 240 static final int STATE_LOADING = 1; 241 static final int STATE_COMPLETED = 2; 242 static final int STATE_ERROR = 3; 243 static final int STATE_CANCELLED = 4; 244 245 final MtpManager mManager; 246 final MtpDatabase mDatabase; 247 final int[] mOperationsSupported; 248 final Identifier mIdentifier; 249 int[] mObjectHandles; 250 int mState; 251 Date mLastNotified; 252 int mPosition; 253 IOException mError; 254 LoaderTask(MtpManager manager, MtpDatabase database, int[] operationsSupported, Identifier identifier)255 LoaderTask(MtpManager manager, MtpDatabase database, int[] operationsSupported, 256 Identifier identifier) { 257 assert operationsSupported != null; 258 assert identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE; 259 mManager = manager; 260 mDatabase = database; 261 mOperationsSupported = operationsSupported; 262 mIdentifier = identifier; 263 mObjectHandles = null; 264 mState = STATE_START; 265 mPosition = 0; 266 mLastNotified = new Date(); 267 } 268 loadObjectHandles()269 synchronized void loadObjectHandles() { 270 assert mState == STATE_START; 271 mPosition = 0; 272 int parentHandle = mIdentifier.mObjectHandle; 273 // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to 274 // getObjectHandles if we would like to obtain children under the root. 275 if (mIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) { 276 parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN; 277 } 278 try { 279 mObjectHandles = mManager.getObjectHandles( 280 mIdentifier.mDeviceId, mIdentifier.mStorageId, parentHandle); 281 mState = STATE_LOADING; 282 } catch (IOException error) { 283 mError = error; 284 mState = STATE_ERROR; 285 } 286 } 287 288 /** 289 * Returns a cursor that traverses the child document of the parent document handled by the 290 * task. 291 * The returned task may have a EXTRA_LOADING flag. 292 */ createCursor(ContentResolver resolver, String[] columnNames)293 synchronized Cursor createCursor(ContentResolver resolver, String[] columnNames) 294 throws IOException { 295 final Bundle extras = new Bundle(); 296 switch (getState()) { 297 case STATE_LOADING: 298 extras.putBoolean(DocumentsContract.EXTRA_LOADING, true); 299 break; 300 case STATE_ERROR: 301 throw mError; 302 } 303 final Cursor cursor = 304 mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId); 305 cursor.setExtras(extras); 306 cursor.setNotificationUri(resolver, createUri()); 307 return cursor; 308 } 309 310 /** 311 * Stores object information into database. 312 */ loadObjectInfoList(int count)313 void loadObjectInfoList(int count) { 314 synchronized (this) { 315 if (mState != STATE_LOADING) { 316 return; 317 } 318 if (mPosition == 0) { 319 try{ 320 mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId); 321 } catch (FileNotFoundException error) { 322 mError = error; 323 mState = STATE_ERROR; 324 return; 325 } 326 } 327 } 328 final ArrayList<MtpObjectInfo> infoList = new ArrayList<>(); 329 for (int chunkEnd = mPosition + count; 330 mPosition < mObjectHandles.length && mPosition < chunkEnd; 331 mPosition++) { 332 try { 333 infoList.add(mManager.getObjectInfo( 334 mIdentifier.mDeviceId, mObjectHandles[mPosition])); 335 } catch (IOException error) { 336 Log.e(MtpDocumentsProvider.TAG, "Failed to load object info", error); 337 } 338 } 339 final long[] objectSizeList = new long[infoList.size()]; 340 for (int i = 0; i < infoList.size(); i++) { 341 final MtpObjectInfo info = infoList.get(i); 342 // Compressed size is 32-bit unsigned integer but getCompressedSize returns the 343 // value in Java int (signed 32-bit integer). Use getCompressedSizeLong instead 344 // to get the value in Java long. 345 if (info.getCompressedSizeLong() != 0xffffffffl) { 346 objectSizeList[i] = info.getCompressedSizeLong(); 347 continue; 348 } 349 350 if (!MtpDeviceRecord.isSupported( 351 mOperationsSupported, 352 MtpConstants.OPERATION_GET_OBJECT_PROP_DESC) || 353 !MtpDeviceRecord.isSupported( 354 mOperationsSupported, 355 MtpConstants.OPERATION_GET_OBJECT_PROP_VALUE)) { 356 objectSizeList[i] = -1; 357 continue; 358 } 359 360 // Object size is more than 4GB. 361 try { 362 objectSizeList[i] = mManager.getObjectSizeLong( 363 mIdentifier.mDeviceId, 364 info.getObjectHandle(), 365 info.getFormat()); 366 } catch (IOException error) { 367 Log.e(MtpDocumentsProvider.TAG, "Failed to get object size property.", error); 368 objectSizeList[i] = -1; 369 } 370 } 371 synchronized (this) { 372 // Check if the task is cancelled or not. 373 if (mState != STATE_LOADING) { 374 return; 375 } 376 try { 377 mDatabase.getMapper().putChildDocuments( 378 mIdentifier.mDeviceId, 379 mIdentifier.mDocumentId, 380 mOperationsSupported, 381 infoList.toArray(new MtpObjectInfo[infoList.size()]), 382 objectSizeList); 383 } catch (FileNotFoundException error) { 384 // Looks like the parent document information is removed. 385 // Adding documents has already cancelled in Mapper so we don't need to invoke 386 // stopAddingDocuments. 387 mError = error; 388 mState = STATE_ERROR; 389 return; 390 } 391 if (mPosition >= mObjectHandles.length) { 392 try{ 393 mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId); 394 mState = STATE_COMPLETED; 395 } catch (FileNotFoundException error) { 396 mError = error; 397 mState = STATE_ERROR; 398 return; 399 } 400 } 401 } 402 } 403 404 /** 405 * Cancels the task. 406 */ cancel()407 synchronized void cancel() { 408 mDatabase.getMapper().cancelAddingDocuments(mIdentifier.mDocumentId); 409 mState = STATE_CANCELLED; 410 } 411 412 /** 413 * Returns a state of the task. 414 */ getState()415 int getState() { 416 return mState; 417 } 418 419 /** 420 * Notifies a change of child list of the document. 421 */ notify(ContentResolver resolver)422 void notify(ContentResolver resolver) { 423 resolver.notifyChange(createUri(), null, false); 424 mLastNotified = new Date(); 425 } 426 createUri()427 private Uri createUri() { 428 return DocumentsContract.buildChildDocumentsUri( 429 MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId); 430 } 431 } 432 } 433