• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
getInstance()96     static MtpDocumentsProvider getInstance() {
97         return sSingleton;
98     }
99 
100     @Override
onCreate()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
onCreateForTesting( Context context, Resources resources, MtpManager mtpManager, ContentResolver resolver, MtpDatabase database, StorageManager storageManager, ServiceIntentSender intentSender)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
queryRoots(String[] projection)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
queryDocument(String documentId, String[] projection)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
queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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
openDocument( String documentId, String mode, CancellationSignal signal)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
openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)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
deleteDocument(String documentId)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
onTrimMemory(int level)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
createDocument(String parentDocumentId, String mimeType, String displayName)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 
353             final int parentObjectHandle;
354             final int storageId;
355             switch (parentId.mDocumentType) {
356                 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
357                     final String[] storageDocumentIds =
358                             mDatabase.getStorageDocumentIds(parentId.mDocumentId);
359                     if (storageDocumentIds.length == 1) {
360                         final String newDocumentId =
361                                 createDocument(storageDocumentIds[0], mimeType, displayName);
362                         notifyChildDocumentsChange(parentDocumentId);
363                         return newDocumentId;
364                     } else {
365                         throw new UnsupportedOperationException(
366                                 "Cannot create a file under the device.");
367                     }
368                 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE:
369                     storageId = parentId.mStorageId;
370                     parentObjectHandle = -1;
371                     break;
372                 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
373                     storageId = parentId.mStorageId;
374                     parentObjectHandle = parentId.mObjectHandle;
375                     break;
376                 default:
377                     throw new IllegalArgumentException("Unexpected document type.");
378             }
379 
380             pipe = ParcelFileDescriptor.createReliablePipe();
381             int objectHandle = -1;
382             MtpObjectInfo info = null;
383             try {
384                 pipe[0].close();  // 0 bytes for a new document.
385 
386                 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
387                         MtpConstants.FORMAT_ASSOCIATION :
388                         MediaFile.getFormatCode(displayName, mimeType);
389                 info = new MtpObjectInfo.Builder()
390                         .setStorageId(storageId)
391                         .setParent(parentObjectHandle)
392                         .setFormat(formatCode)
393                         .setName(displayName)
394                         .build();
395 
396                 final String[] parts = FileUtils.splitFileName(mimeType, displayName);
397                 final String baseName = parts[0];
398                 final String extension = parts[1];
399                 for (int i = 0; i <= 32; i++) {
400                     final MtpObjectInfo infoUniqueName;
401                     if (i == 0) {
402                         infoUniqueName = info;
403                     } else {
404                         String suffixedName = baseName + " (" + i + " )";
405                         if (!extension.isEmpty()) {
406                             suffixedName += "." + extension;
407                         }
408                         infoUniqueName =
409                                 new MtpObjectInfo.Builder(info).setName(suffixedName).build();
410                     }
411                     try {
412                         objectHandle = mMtpManager.createDocument(
413                                 parentId.mDeviceId, infoUniqueName, pipe[1]);
414                         break;
415                     } catch (SendObjectInfoFailure exp) {
416                         // This can be caused when we have an existing file with the same name.
417                         continue;
418                     }
419                 }
420             } finally {
421                 pipe[1].close();
422             }
423             if (objectHandle == -1) {
424                 throw new IllegalArgumentException(
425                         "The file name \"" + displayName + "\" is conflicted with existing files " +
426                         "and the provider failed to find unique name.");
427             }
428             final MtpObjectInfo infoWithHandle =
429                     new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
430             final String documentId = mDatabase.putNewDocument(
431                     parentId.mDeviceId, parentDocumentId, record.operationsSupported,
432                     infoWithHandle, 0l);
433             getDocumentLoader(parentId).cancelTask(parentId);
434             notifyChildDocumentsChange(parentDocumentId);
435             return documentId;
436         } catch (FileNotFoundException | RuntimeException error) {
437             Log.e(TAG, "createDocument", error);
438             throw error;
439         } catch (IOException error) {
440             Log.e(TAG, "createDocument", error);
441             throw new IllegalStateException(error);
442         }
443     }
444 
445     @Override
isChildDocument(String parentDocumentId, String documentId)446     public boolean isChildDocument(String parentDocumentId, String documentId) {
447         try {
448             Identifier identifier = mDatabase.createIdentifier(documentId);
449             while (true) {
450                 if (parentDocumentId.equals(identifier.mDocumentId)) {
451                     return true;
452                 }
453                 if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
454                     return false;
455                 }
456                 identifier = mDatabase.getParentIdentifier(identifier.mDocumentId);
457             }
458         } catch (FileNotFoundException error) {
459             return false;
460         }
461     }
462 
openDevice(int deviceId)463     void openDevice(int deviceId) throws IOException {
464         synchronized (mDeviceListLock) {
465             if (mDeviceToolkits.containsKey(deviceId)) {
466                 return;
467             }
468             if (DEBUG) {
469                 Log.d(TAG, "Open device " + deviceId);
470             }
471             final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
472             final DeviceToolkit toolkit =
473                     new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
474             mDeviceToolkits.put(deviceId, toolkit);
475             mIntentSender.sendUpdateNotificationIntent();
476             try {
477                 mRootScanner.resume().await();
478             } catch (InterruptedException error) {
479                 Log.e(TAG, "openDevice", error);
480             }
481             // Resume document loader to remap disconnected document ID. Must be invoked after the
482             // root scanner resumes.
483             toolkit.mDocumentLoader.resume();
484         }
485     }
486 
closeDevice(int deviceId)487     void closeDevice(int deviceId) throws IOException, InterruptedException {
488         synchronized (mDeviceListLock) {
489             closeDeviceInternal(deviceId);
490         }
491         mRootScanner.resume();
492         mIntentSender.sendUpdateNotificationIntent();
493     }
494 
getOpenedDeviceRecordsCache()495     MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
496         synchronized (mDeviceListLock) {
497             final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
498             int i = 0;
499             for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
500                 records[i] = toolkit.mDeviceRecord;
501                 i++;
502             }
503             return records;
504         }
505     }
506 
507     /**
508      * Obtains document ID for the given device ID.
509      * @param deviceId
510      * @return document ID
511      * @throws FileNotFoundException device ID has not been build.
512      */
getDeviceDocumentId(int deviceId)513     public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
514         return mDatabase.getDeviceDocumentId(deviceId);
515     }
516 
517     /**
518      * Resumes root scanner to handle the update of device list.
519      */
resumeRootScanner()520     void resumeRootScanner() {
521         if (DEBUG) {
522             Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
523         }
524         mRootScanner.resume();
525     }
526 
527     /**
528      * Finalize the content provider for unit tests.
529      */
530     @Override
shutdown()531     public void shutdown() {
532         synchronized (mDeviceListLock) {
533             try {
534                 // Copy the opened key set because it will be modified when closing devices.
535                 final Integer[] keySet =
536                         mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
537                 for (final int id : keySet) {
538                     closeDeviceInternal(id);
539                 }
540                 mRootScanner.pause();
541             } catch (InterruptedException | IOException | TimeoutException e) {
542                 // It should fail unit tests by throwing runtime exception.
543                 throw new RuntimeException(e);
544             } finally {
545                 mDatabase.close();
546                 mAppFuse.close();
547                 super.shutdown();
548             }
549         }
550     }
551 
notifyChildDocumentsChange(String parentDocumentId)552     private void notifyChildDocumentsChange(String parentDocumentId) {
553         mResolver.notifyChange(
554                 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
555                 null,
556                 false);
557     }
558 
559     /**
560      * Clears MTP identifier in the database.
561      */
resume()562     private void resume() {
563         synchronized (mDeviceListLock) {
564             mDatabase.getMapper().clearMapping();
565         }
566     }
567 
closeDeviceInternal(int deviceId)568     private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
569         // TODO: Flush the device before closing (if not closed externally).
570         if (!mDeviceToolkits.containsKey(deviceId)) {
571             return;
572         }
573         if (DEBUG) {
574             Log.d(TAG, "Close device " + deviceId);
575         }
576         getDeviceToolkit(deviceId).close();
577         mDeviceToolkits.remove(deviceId);
578         mMtpManager.closeDevice(deviceId);
579     }
580 
getDeviceToolkit(int deviceId)581     private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
582         synchronized (mDeviceListLock) {
583             final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
584             if (toolkit == null) {
585                 throw new FileNotFoundException();
586             }
587             return toolkit;
588         }
589     }
590 
getPipeManager(Identifier identifier)591     private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
592         return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
593     }
594 
getDocumentLoader(Identifier identifier)595     private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
596         return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
597     }
598 
getFileSize(String documentId)599     private long getFileSize(String documentId) throws FileNotFoundException {
600         final Cursor cursor = mDatabase.queryDocument(
601                 documentId,
602                 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
603         try {
604             if (cursor.moveToNext()) {
605                 if (cursor.isNull(0)) {
606                     throw new UnsupportedOperationException();
607                 }
608                 return cursor.getLong(0);
609             } else {
610                 throw new FileNotFoundException();
611             }
612         } finally {
613             cursor.close();
614         }
615     }
616 
617     /**
618      * Creates empty cursor with specific error message.
619      *
620      * @param projection Column names.
621      * @param stringResId String resource ID of error message.
622      * @return Empty cursor with error message.
623      */
createErrorCursor(String[] projection, int stringResId)624     private Cursor createErrorCursor(String[] projection, int stringResId) {
625         final Bundle bundle = new Bundle();
626         bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
627         final Cursor cursor = new MatrixCursor(projection);
628         cursor.setExtras(bundle);
629         return cursor;
630     }
631 
632     private static class DeviceToolkit implements AutoCloseable {
633         public final PipeManager mPipeManager;
634         public final DocumentLoader mDocumentLoader;
635         public final MtpDeviceRecord mDeviceRecord;
636 
DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database, MtpDeviceRecord record)637         public DeviceToolkit(MtpManager manager,
638                              ContentResolver resolver,
639                              MtpDatabase database,
640                              MtpDeviceRecord record) {
641             mPipeManager = new PipeManager(database);
642             mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
643             mDeviceRecord = record;
644         }
645 
646         @Override
close()647         public void close() throws InterruptedException {
648             mPipeManager.close();
649             mDocumentLoader.close();
650         }
651     }
652 
653     private class AppFuseCallback implements AppFuse.Callback {
654         private final Map<Long, MtpFileWriter> mWriters = new HashMap<>();
655 
656         @Override
getFileSize(int inode)657         public long getFileSize(int inode) throws FileNotFoundException {
658             return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
659         }
660 
661         @Override
readObjectBytes( int inode, long offset, long size, byte[] buffer)662         public long readObjectBytes(
663                 int inode, long offset, long size, byte[] buffer) throws IOException {
664             final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
665             final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
666 
667             if (MtpDeviceRecord.isSupported(
668                     record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
669                 return mMtpManager.getPartialObject64(
670                         identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
671             }
672 
673             if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
674                     record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
675                 return mMtpManager.getPartialObject(
676                         identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
677             }
678 
679             throw new UnsupportedOperationException();
680         }
681 
682         @Override
writeObjectBytes( long fileHandle, int inode, long offset, int size, byte[] bytes)683         public int writeObjectBytes(
684                 long fileHandle, int inode, long offset, int size, byte[] bytes)
685                 throws IOException, ErrnoException {
686             final MtpFileWriter writer;
687             if (mWriters.containsKey(fileHandle)) {
688                 writer = mWriters.get(fileHandle);
689             } else {
690                 writer = new MtpFileWriter(mContext, String.valueOf(inode));
691                 mWriters.put(fileHandle, writer);
692             }
693             return writer.write(offset, size, bytes);
694         }
695 
696         @Override
flushFileHandle(long fileHandle)697         public void flushFileHandle(long fileHandle) throws IOException, ErrnoException {
698             final MtpFileWriter writer = mWriters.get(fileHandle);
699             if (writer == null) {
700                 // File handle for reading.
701                 return;
702             }
703             final MtpDeviceRecord device = getDeviceToolkit(
704                     mDatabase.createIdentifier(writer.getDocumentId()).mDeviceId).mDeviceRecord;
705             writer.flush(mMtpManager, mDatabase, device.operationsSupported);
706         }
707 
708         @Override
closeFileHandle(long fileHandle)709         public void closeFileHandle(long fileHandle) throws IOException, ErrnoException {
710             final MtpFileWriter writer = mWriters.get(fileHandle);
711             if (writer == null) {
712                 // File handle for reading.
713                 return;
714             }
715             try {
716                 writer.close();
717             } finally {
718                 mWriters.remove(fileHandle);
719             }
720         }
721     }
722 }
723