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.content.ContentResolver;
     20 import android.content.ContentValues;
     21 import android.content.Context;
     22 import android.content.UriPermission;
     23 import android.content.res.AssetFileDescriptor;
     24 import android.content.res.Resources;
     25 import android.database.Cursor;
     26 import android.database.DatabaseUtils;
     27 import android.database.MatrixCursor;
     28 import android.database.sqlite.SQLiteDiskIOException;
     29 import android.graphics.Point;
     30 import android.media.MediaFile;
     31 import android.mtp.MtpConstants;
     32 import android.mtp.MtpObjectInfo;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 import android.os.CancellationSignal;
     36 import android.os.FileUtils;
     37 import android.os.ParcelFileDescriptor;
     38 import android.os.ProxyFileDescriptorCallback;
     39 import android.os.storage.StorageManager;
     40 import android.provider.DocumentsContract.Document;
     41 import android.provider.DocumentsContract.Path;
     42 import android.provider.DocumentsContract.Root;
     43 import android.provider.DocumentsContract;
     44 import android.provider.DocumentsProvider;
     45 import android.provider.Settings;
     46 import android.system.ErrnoException;
     47 import android.system.OsConstants;
     48 import android.util.Log;
     49 
     50 import com.android.internal.annotations.GuardedBy;
     51 import com.android.internal.annotations.VisibleForTesting;
     52 
     53 import java.io.FileNotFoundException;
     54 import java.io.IOException;
     55 import java.util.HashMap;
     56 import java.util.LinkedList;
     57 import java.util.List;
     58 import java.util.Map;
     59 import java.util.concurrent.TimeoutException;
     60 import libcore.io.IoUtils;
     61 
     62 /**
     63  * DocumentsProvider for MTP devices.
     64  */
     65 public class MtpDocumentsProvider extends DocumentsProvider {
     66     static final String AUTHORITY = "com.android.mtp.documents";
     67     static final String TAG = "MtpDocumentsProvider";
     68     static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
     69             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
     70             Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
     71             Root.COLUMN_AVAILABLE_BYTES,
     72     };
     73     static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
     74             Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
     75             Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
     76             Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     77     };
     78 
     79     static final boolean DEBUG = false;
     80 
     81     private final Object mDeviceListLock = new Object();
     82 
     83     private static MtpDocumentsProvider sSingleton;
     84 
     85     private MtpManager mMtpManager;
     86     private ContentResolver mResolver;
     87     @GuardedBy("mDeviceListLock")
     88     private Map<Integer, DeviceToolkit> mDeviceToolkits;
     89     private RootScanner mRootScanner;
     90     private Resources mResources;
     91     private MtpDatabase mDatabase;
     92     private ServiceIntentSender mIntentSender;
     93     private Context mContext;
     94     private StorageManager mStorageManager;
     95 
     96     /**
     97      * Provides singleton instance to MtpDocumentsService.
     98      */
     99     static MtpDocumentsProvider getInstance() {
    100         return sSingleton;
    101     }
    102 
    103     @Override
    104     public boolean onCreate() {
    105         sSingleton = this;
    106         mContext = getContext();
    107         mResources = getContext().getResources();
    108         mMtpManager = new MtpManager(getContext());
    109         mResolver = getContext().getContentResolver();
    110         mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
    111         mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
    112         mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
    113         mIntentSender = new ServiceIntentSender(getContext());
    114         mStorageManager = getContext().getSystemService(StorageManager.class);
    115 
    116         // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider
    117         // after booting.
    118         try {
    119             final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1);
    120             final int lastBootCount = mDatabase.getLastBootCount();
    121             if (bootCount != -1 && bootCount != lastBootCount) {
    122                 mDatabase.setLastBootCount(bootCount);
    123                 final List<UriPermission> permissions =
    124                         mResolver.getOutgoingPersistedUriPermissions();
    125                 final Uri[] uris = new Uri[permissions.size()];
    126                 for (int i = 0; i < permissions.size(); i++) {
    127                     uris[i] = permissions.get(i).getUri();
    128                 }
    129                 mDatabase.cleanDatabase(uris);
    130             }
    131         } catch (SQLiteDiskIOException error) {
    132             // It can happen due to disk shortage.
    133             Log.e(TAG, "Failed to clean database.", error);
    134             return false;
    135         }
    136 
    137         resume();
    138         return true;
    139     }
    140 
    141     @VisibleForTesting
    142     boolean onCreateForTesting(
    143             Context context,
    144             Resources resources,
    145             MtpManager mtpManager,
    146             ContentResolver resolver,
    147             MtpDatabase database,
    148             StorageManager storageManager,
    149             ServiceIntentSender intentSender) {
    150         mContext = context;
    151         mResources = resources;
    152         mMtpManager = mtpManager;
    153         mResolver = resolver;
    154         mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
    155         mDatabase = database;
    156         mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
    157         mIntentSender = intentSender;
    158         mStorageManager = storageManager;
    159 
    160         resume();
    161         return true;
    162     }
    163 
    164     @Override
    165     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
    166         if (projection == null) {
    167             projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
    168         }
    169         final Cursor cursor = mDatabase.queryRoots(mResources, projection);
    170         cursor.setNotificationUri(
    171                 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
    172         return cursor;
    173     }
    174 
    175     @Override
    176     public Cursor queryDocument(String documentId, String[] projection)
    177             throws FileNotFoundException {
    178         if (projection == null) {
    179             projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
    180         }
    181         final Cursor cursor = mDatabase.queryDocument(documentId, projection);
    182         final int cursorCount = cursor.getCount();
    183         if (cursorCount == 0) {
    184             cursor.close();
    185             throw new FileNotFoundException();
    186         } else if (cursorCount != 1) {
    187             cursor.close();
    188             Log.wtf(TAG, "Unexpected cursor size: " + cursorCount);
    189             return null;
    190         }
    191 
    192         final Identifier identifier = mDatabase.createIdentifier(documentId);
    193         if (identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
    194             return cursor;
    195         }
    196         final String[] storageDocIds = mDatabase.getStorageDocumentIds(documentId);
    197         if (storageDocIds.length != 1) {
    198             return mDatabase.queryDocument(documentId, projection);
    199         }
    200 
    201         // If the documentId specifies a device having exact one storage, we repalce some device
    202         // attributes with the storage attributes.
    203         try {
    204             final String storageName;
    205             final int storageFlags;
    206             try (final Cursor storageCursor = mDatabase.queryDocument(
    207                     storageDocIds[0],
    208                     MtpDatabase.strings(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS))) {
    209                 if (!storageCursor.moveToNext()) {
    210                     throw new FileNotFoundException();
    211                 }
    212                 storageName = storageCursor.getString(0);
    213                 storageFlags = storageCursor.getInt(1);
    214             }
    215 
    216             cursor.moveToNext();
    217             final ContentValues values = new ContentValues();
    218             DatabaseUtils.cursorRowToContentValues(cursor, values);
    219             if (values.containsKey(Document.COLUMN_DISPLAY_NAME)) {
    220                 values.put(Document.COLUMN_DISPLAY_NAME, mResources.getString(
    221                         R.string.root_name,
    222                         values.getAsString(Document.COLUMN_DISPLAY_NAME),
    223                         storageName));
    224             }
    225             values.put(Document.COLUMN_FLAGS, storageFlags);
    226             final MatrixCursor output = new MatrixCursor(projection, 1);
    227             MtpDatabase.putValuesToCursor(values, output);
    228             return output;
    229         } finally {
    230             cursor.close();
    231         }
    232     }
    233 
    234     @Override
    235     public Cursor queryChildDocuments(String parentDocumentId,
    236             String[] projection, String sortOrder) throws FileNotFoundException {
    237         if (DEBUG) {
    238             Log.d(TAG, "queryChildDocuments: " + parentDocumentId);
    239         }
    240         if (projection == null) {
    241             projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
    242         }
    243         Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
    244         try {
    245             openDevice(parentIdentifier.mDeviceId);
    246             if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
    247                 final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId);
    248                 if (storageDocIds.length == 0) {
    249                     // Remote device does not provide storages. Maybe it is locked.
    250                     return createErrorCursor(projection, R.string.error_locked_device);
    251                 } else if (storageDocIds.length > 1) {
    252                     // Returns storage list from database.
    253                     return mDatabase.queryChildDocuments(projection, parentDocumentId);
    254                 }
    255 
    256                 // Exact one storage is found. Skip storage and returns object in the single
    257                 // storage.
    258                 parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]);
    259             }
    260 
    261             // Returns object list from document loader.
    262             return getDocumentLoader(parentIdentifier).queryChildDocuments(
    263                     projection, parentIdentifier);
    264         } catch (BusyDeviceException exception) {
    265             return createErrorCursor(projection, R.string.error_busy_device);
    266         } catch (IOException exception) {
    267             Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception);
    268             throw new FileNotFoundException(exception.getMessage());
    269         }
    270     }
    271 
    272     @Override
    273     public ParcelFileDescriptor openDocument(
    274             String documentId, String mode, CancellationSignal signal)
    275                     throws FileNotFoundException {
    276         if (DEBUG) {
    277             Log.d(TAG, "openDocument: " + documentId);
    278         }
    279         final Identifier identifier = mDatabase.createIdentifier(documentId);
    280         try {
    281             openDevice(identifier.mDeviceId);
    282             final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
    283             // Turn off MODE_CREATE because openDocument does not allow to create new files.
    284             final int modeFlag =
    285                     ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
    286             if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
    287                 long fileSize;
    288                 try {
    289                     fileSize = getFileSize(documentId);
    290                 } catch (UnsupportedOperationException exception) {
    291                     fileSize = -1;
    292                 }
    293                 if (MtpDeviceRecord.isPartialReadSupported(
    294                         device.operationsSupported, fileSize)) {
    295 
    296                     return mStorageManager.openProxyFileDescriptor(
    297                             modeFlag,
    298                             new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
    299                 } else {
    300                     // If getPartialObject{|64} are not supported for the device, returns
    301                     // non-seekable pipe FD instead.
    302                     return getPipeManager(identifier).readDocument(mMtpManager, identifier);
    303                 }
    304             } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
    305                 // TODO: Clear the parent document loader task (if exists) and call notify
    306                 // when writing is completed.
    307                 if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
    308                     return mStorageManager.openProxyFileDescriptor(
    309                             modeFlag,
    310                             new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
    311                 } else {
    312                     throw new UnsupportedOperationException(
    313                             "The device does not support writing operation.");
    314                 }
    315             } else {
    316                 // TODO: Add support for "rw" mode.
    317                 throw new UnsupportedOperationException("The provider does not support 'rw' mode.");
    318             }
    319         } catch (FileNotFoundException | RuntimeException error) {
    320             Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
    321             throw error;
    322         } catch (IOException error) {
    323             Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
    324             throw new IllegalStateException(error);
    325         }
    326     }
    327 
    328     @Override
    329     public AssetFileDescriptor openDocumentThumbnail(
    330             String documentId,
    331             Point sizeHint,
    332             CancellationSignal signal) throws FileNotFoundException {
    333         final Identifier identifier = mDatabase.createIdentifier(documentId);
    334         try {
    335             openDevice(identifier.mDeviceId);
    336             return new AssetFileDescriptor(
    337                     getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
    338                     0,  // Start offset.
    339                     AssetFileDescriptor.UNKNOWN_LENGTH);
    340         } catch (IOException error) {
    341             Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
    342             throw new FileNotFoundException(error.getMessage());
    343         }
    344     }
    345 
    346     @Override
    347     public void deleteDocument(String documentId) throws FileNotFoundException {
    348         try {
    349             final Identifier identifier = mDatabase.createIdentifier(documentId);
    350             openDevice(identifier.mDeviceId);
    351             final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
    352             mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
    353             mDatabase.deleteDocument(documentId);
    354             getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier);
    355             notifyChildDocumentsChange(parentIdentifier.mDocumentId);
    356             if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
    357                 // If the parent is storage, the object might be appeared as child of device because
    358                 // we skip storage when the device has only one storage.
    359                 final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
    360                         parentIdentifier.mDocumentId);
    361                 notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
    362             }
    363         } catch (IOException error) {
    364             Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
    365             throw new FileNotFoundException(error.getMessage());
    366         }
    367     }
    368 
    369     @Override
    370     public void onTrimMemory(int level) {
    371         synchronized (mDeviceListLock) {
    372             for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
    373                 toolkit.mDocumentLoader.clearCompletedTasks();
    374             }
    375         }
    376     }
    377 
    378     @Override
    379     public String createDocument(String parentDocumentId, String mimeType, String displayName)
    380             throws FileNotFoundException {
    381         if (DEBUG) {
    382             Log.d(TAG, "createDocument: " + displayName);
    383         }
    384         final Identifier parentId;
    385         final MtpDeviceRecord record;
    386         final ParcelFileDescriptor[] pipe;
    387         try {
    388             parentId = mDatabase.createIdentifier(parentDocumentId);
    389             openDevice(parentId.mDeviceId);
    390             record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord;
    391             if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) {
    392                 throw new UnsupportedOperationException(
    393                         "Writing operation is not supported by the device.");
    394             }
    395 
    396             final int parentObjectHandle;
    397             final int storageId;
    398             switch (parentId.mDocumentType) {
    399                 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
    400                     final String[] storageDocumentIds =
    401                             mDatabase.getStorageDocumentIds(parentId.mDocumentId);
    402                     if (storageDocumentIds.length == 1) {
    403                         final String newDocumentId =
    404                                 createDocument(storageDocumentIds[0], mimeType, displayName);
    405                         notifyChildDocumentsChange(parentDocumentId);
    406                         return newDocumentId;
    407                     } else {
    408                         throw new UnsupportedOperationException(
    409                                 "Cannot create a file under the device.");
    410                     }
    411                 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE:
    412                     storageId = parentId.mStorageId;
    413                     parentObjectHandle = -1;
    414                     break;
    415                 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
    416                     storageId = parentId.mStorageId;
    417                     parentObjectHandle = parentId.mObjectHandle;
    418                     break;
    419                 default:
    420                     throw new IllegalArgumentException("Unexpected document type.");
    421             }
    422 
    423             pipe = ParcelFileDescriptor.createReliablePipe();
    424             int objectHandle = -1;
    425             MtpObjectInfo info = null;
    426             try {
    427                 pipe[0].close();  // 0 bytes for a new document.
    428 
    429                 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
    430                         MtpConstants.FORMAT_ASSOCIATION :
    431                         MediaFile.getFormatCode(displayName, mimeType);
    432                 info = new MtpObjectInfo.Builder()
    433                         .setStorageId(storageId)
    434                         .setParent(parentObjectHandle)
    435                         .setFormat(formatCode)
    436                         .setName(displayName)
    437                         .build();
    438 
    439                 final String[] parts = FileUtils.splitFileName(mimeType, displayName);
    440                 final String baseName = parts[0];
    441                 final String extension = parts[1];
    442                 for (int i = 0; i <= 32; i++) {
    443                     final MtpObjectInfo infoUniqueName;
    444                     if (i == 0) {
    445                         infoUniqueName = info;
    446                     } else {
    447                         String suffixedName = baseName + " (" + i + " )";
    448                         if (!extension.isEmpty()) {
    449                             suffixedName += "." + extension;
    450                         }
    451                         infoUniqueName =
    452                                 new MtpObjectInfo.Builder(info).setName(suffixedName).build();
    453                     }
    454                     try {
    455                         objectHandle = mMtpManager.createDocument(
    456                                 parentId.mDeviceId, infoUniqueName, pipe[1]);
    457                         break;
    458                     } catch (SendObjectInfoFailure exp) {
    459                         // This can be caused when we have an existing file with the same name.
    460                         continue;
    461                     }
    462                 }
    463             } finally {
    464                 pipe[1].close();
    465             }
    466             if (objectHandle == -1) {
    467                 throw new IllegalArgumentException(
    468                         "The file name \"" + displayName + "\" is conflicted with existing files " +
    469                         "and the provider failed to find unique name.");
    470             }
    471             final MtpObjectInfo infoWithHandle =
    472                     new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
    473             final String documentId = mDatabase.putNewDocument(
    474                     parentId.mDeviceId, parentDocumentId, record.operationsSupported,
    475                     infoWithHandle, 0l);
    476             getDocumentLoader(parentId).cancelTask(parentId);
    477             notifyChildDocumentsChange(parentDocumentId);
    478             return documentId;
    479         } catch (FileNotFoundException | RuntimeException error) {
    480             Log.e(TAG, "createDocument", error);
    481             throw error;
    482         } catch (IOException error) {
    483             Log.e(TAG, "createDocument", error);
    484             throw new IllegalStateException(error);
    485         }
    486     }
    487 
    488     @Override
    489     public Path findDocumentPath(String parentDocumentId, String childDocumentId)
    490             throws FileNotFoundException {
    491         final LinkedList<String> ids = new LinkedList<>();
    492         final Identifier childIdentifier = mDatabase.createIdentifier(childDocumentId);
    493 
    494         Identifier i = childIdentifier;
    495         outer: while (true) {
    496             if (i.mDocumentId.equals(parentDocumentId)) {
    497                 ids.addFirst(i.mDocumentId);
    498                 break;
    499             }
    500             switch (i.mDocumentType) {
    501                 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
    502                     ids.addFirst(i.mDocumentId);
    503                     i = mDatabase.getParentIdentifier(i.mDocumentId);
    504                     break;
    505                 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: {
    506                     // Check if there is the multiple storage.
    507                     final Identifier deviceIdentifier =
    508                             mDatabase.getParentIdentifier(i.mDocumentId);
    509                     final String[] storageIds =
    510                             mDatabase.getStorageDocumentIds(deviceIdentifier.mDocumentId);
    511                     // Add storage's document ID to the path only when the device has multiple
    512                     // storages.
    513                     if (storageIds.length > 1) {
    514                         ids.addFirst(i.mDocumentId);
    515                         break outer;
    516                     }
    517                     i = deviceIdentifier;
    518                     break;
    519                 }
    520                 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
    521                     ids.addFirst(i.mDocumentId);
    522                     break outer;
    523             }
    524         }
    525 
    526         if (parentDocumentId != null) {
    527             return new Path(null, ids);
    528         } else {
    529             return new Path(/* Should be same with root ID */ i.mDocumentId, ids);
    530         }
    531     }
    532 
    533     @Override
    534     public boolean isChildDocument(String parentDocumentId, String documentId) {
    535         try {
    536             Identifier identifier = mDatabase.createIdentifier(documentId);
    537             while (true) {
    538                 if (parentDocumentId.equals(identifier.mDocumentId)) {
    539                     return true;
    540                 }
    541                 if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
    542                     return false;
    543                 }
    544                 identifier = mDatabase.getParentIdentifier(identifier.mDocumentId);
    545             }
    546         } catch (FileNotFoundException error) {
    547             return false;
    548         }
    549     }
    550 
    551     void openDevice(int deviceId) throws IOException {
    552         synchronized (mDeviceListLock) {
    553             if (mDeviceToolkits.containsKey(deviceId)) {
    554                 return;
    555             }
    556             if (DEBUG) {
    557                 Log.d(TAG, "Open device " + deviceId);
    558             }
    559             final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
    560             final DeviceToolkit toolkit =
    561                     new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
    562             mDeviceToolkits.put(deviceId, toolkit);
    563             mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
    564             try {
    565                 mRootScanner.resume().await();
    566             } catch (InterruptedException error) {
    567                 Log.e(TAG, "openDevice", error);
    568             }
    569             // Resume document loader to remap disconnected document ID. Must be invoked after the
    570             // root scanner resumes.
    571             toolkit.mDocumentLoader.resume();
    572         }
    573     }
    574 
    575     void closeDevice(int deviceId) throws IOException, InterruptedException {
    576         synchronized (mDeviceListLock) {
    577             closeDeviceInternal(deviceId);
    578             mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
    579         }
    580         mRootScanner.resume();
    581     }
    582 
    583     MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
    584         synchronized (mDeviceListLock) {
    585             final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
    586             int i = 0;
    587             for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
    588                 records[i] = toolkit.mDeviceRecord;
    589                 i++;
    590             }
    591             return records;
    592         }
    593     }
    594 
    595     /**
    596      * Obtains document ID for the given device ID.
    597      * @param deviceId
    598      * @return document ID
    599      * @throws FileNotFoundException device ID has not been build.
    600      */
    601     public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
    602         return mDatabase.getDeviceDocumentId(deviceId);
    603     }
    604 
    605     /**
    606      * Resumes root scanner to handle the update of device list.
    607      */
    608     void resumeRootScanner() {
    609         if (DEBUG) {
    610             Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
    611         }
    612         mRootScanner.resume();
    613     }
    614 
    615     /**
    616      * Finalize the content provider for unit tests.
    617      */
    618     @Override
    619     public void shutdown() {
    620         synchronized (mDeviceListLock) {
    621             try {
    622                 // Copy the opened key set because it will be modified when closing devices.
    623                 final Integer[] keySet =
    624                         mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
    625                 for (final int id : keySet) {
    626                     closeDeviceInternal(id);
    627                 }
    628                 mRootScanner.pause();
    629             } catch (InterruptedException | IOException | TimeoutException e) {
    630                 // It should fail unit tests by throwing runtime exception.
    631                 throw new RuntimeException(e);
    632             } finally {
    633                 mDatabase.close();
    634                 super.shutdown();
    635             }
    636         }
    637     }
    638 
    639     private void notifyChildDocumentsChange(String parentDocumentId) {
    640         mResolver.notifyChange(
    641                 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
    642                 null,
    643                 false);
    644     }
    645 
    646     /**
    647      * Clears MTP identifier in the database.
    648      */
    649     private void resume() {
    650         synchronized (mDeviceListLock) {
    651             mDatabase.getMapper().clearMapping();
    652         }
    653     }
    654 
    655     private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
    656         // TODO: Flush the device before closing (if not closed externally).
    657         if (!mDeviceToolkits.containsKey(deviceId)) {
    658             return;
    659         }
    660         if (DEBUG) {
    661             Log.d(TAG, "Close device " + deviceId);
    662         }
    663         getDeviceToolkit(deviceId).close();
    664         mDeviceToolkits.remove(deviceId);
    665         mMtpManager.closeDevice(deviceId);
    666     }
    667 
    668     private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
    669         synchronized (mDeviceListLock) {
    670             final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
    671             if (toolkit == null) {
    672                 throw new FileNotFoundException();
    673             }
    674             return toolkit;
    675         }
    676     }
    677 
    678     private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
    679         return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
    680     }
    681 
    682     private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
    683         return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
    684     }
    685 
    686     private long getFileSize(String documentId) throws FileNotFoundException {
    687         final Cursor cursor = mDatabase.queryDocument(
    688                 documentId,
    689                 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
    690         try {
    691             if (cursor.moveToNext()) {
    692                 if (cursor.isNull(0)) {
    693                     throw new UnsupportedOperationException();
    694                 }
    695                 return cursor.getLong(0);
    696             } else {
    697                 throw new FileNotFoundException();
    698             }
    699         } finally {
    700             cursor.close();
    701         }
    702     }
    703 
    704     /**
    705      * Creates empty cursor with specific error message.
    706      *
    707      * @param projection Column names.
    708      * @param stringResId String resource ID of error message.
    709      * @return Empty cursor with error message.
    710      */
    711     private Cursor createErrorCursor(String[] projection, int stringResId) {
    712         final Bundle bundle = new Bundle();
    713         bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
    714         final Cursor cursor = new MatrixCursor(projection);
    715         cursor.setExtras(bundle);
    716         return cursor;
    717     }
    718 
    719     private static class DeviceToolkit implements AutoCloseable {
    720         public final PipeManager mPipeManager;
    721         public final DocumentLoader mDocumentLoader;
    722         public final MtpDeviceRecord mDeviceRecord;
    723 
    724         public DeviceToolkit(MtpManager manager,
    725                              ContentResolver resolver,
    726                              MtpDatabase database,
    727                              MtpDeviceRecord record) {
    728             mPipeManager = new PipeManager(database);
    729             mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
    730             mDeviceRecord = record;
    731         }
    732 
    733         @Override
    734         public void close() throws InterruptedException {
    735             mPipeManager.close();
    736             mDocumentLoader.close();
    737         }
    738     }
    739 
    740     private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
    741         private final int mInode;
    742         private MtpFileWriter mWriter;
    743 
    744         MtpProxyFileDescriptorCallback(int inode) {
    745             mInode = inode;
    746         }
    747 
    748         @Override
    749         public long onGetSize() throws ErrnoException {
    750             try {
    751                 return getFileSize(String.valueOf(mInode));
    752             } catch (FileNotFoundException e) {
    753                 Log.e(TAG, e.getMessage(), e);
    754                 throw new ErrnoException("onGetSize", OsConstants.ENOENT);
    755             }
    756         }
    757 
    758         @Override
    759         public int onRead(long offset, int size, byte[] data) throws ErrnoException {
    760             try {
    761                 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(mInode));
    762                 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
    763                 if (MtpDeviceRecord.isSupported(
    764                         record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
    765 
    766                         return (int) mMtpManager.getPartialObject64(
    767                                 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
    768 
    769                 }
    770                 if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
    771                         record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
    772                     return (int) mMtpManager.getPartialObject(
    773                             identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
    774                 }
    775                 throw new ErrnoException("onRead", OsConstants.ENOTSUP);
    776             } catch (IOException e) {
    777                 Log.e(TAG, e.getMessage(), e);
    778                 throw new ErrnoException("onRead", OsConstants.EIO);
    779             }
    780         }
    781 
    782         @Override
    783         public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
    784             try {
    785                 if (mWriter == null) {
    786                     mWriter = new MtpFileWriter(mContext, String.valueOf(mInode));
    787                 }
    788                 return mWriter.write(offset, size, data);
    789             } catch (IOException e) {
    790                 Log.e(TAG, e.getMessage(), e);
    791                 throw new ErrnoException("onWrite", OsConstants.EIO);
    792             }
    793         }
    794 
    795         @Override
    796         public void onFsync() throws ErrnoException {
    797             tryFsync();
    798         }
    799 
    800         @Override
    801         public void onRelease() {
    802             try {
    803                 tryFsync();
    804             } catch (ErrnoException error) {
    805                 // Cannot recover from the error at onRelease. Client app should use fsync to
    806                 // ensure the provider writes data correctly.
    807                 Log.e(TAG, "Cannot recover from the error at onRelease.", error);
    808             } finally {
    809                 if (mWriter != null) {
    810                     IoUtils.closeQuietly(mWriter);
    811                 }
    812             }
    813         }
    814 
    815         private void tryFsync() throws ErrnoException {
    816             try {
    817                 if (mWriter != null) {
    818                     final MtpDeviceRecord device =
    819                             getDeviceToolkit(mDatabase.createIdentifier(
    820                                     mWriter.getDocumentId()).mDeviceId).mDeviceRecord;
    821                     mWriter.flush(mMtpManager, mDatabase, device.operationsSupported);
    822                 }
    823             } catch (IOException e) {
    824                 Log.e(TAG, e.getMessage(), e);
    825                 throw new ErrnoException("onWrite", OsConstants.EIO);
    826             }
    827         }
    828     }
    829 }
    830