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