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