Home | History | Annotate | Download | only in mtp
      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 
     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      */
     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      */
    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
    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
    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 
    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      */
    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
    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> {
    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 
    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 
    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 
    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          */
    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          */
    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          */
    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          */
    415         int getState() {
    416             return mState;
    417         }
    418 
    419         /**
    420          * Notifies a change of child list of the document.
    421          */
    422         void notify(ContentResolver resolver) {
    423             resolver.notifyChange(createUri(), null, false);
    424             mLastNotified = new Date();
    425         }
    426 
    427         private Uri createUri() {
    428             return DocumentsContract.buildChildDocumentsUri(
    429                     MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId);
    430         }
    431     }
    432 }
    433