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      */
getInstance()103     static MtpDocumentsProvider getInstance() {
104         return sSingleton;
105     }
106 
107     @Override
onCreate()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
onCreateForTesting( Context context, Resources resources, MtpManager mtpManager, ContentResolver resolver, MtpDatabase database, StorageManager storageManager, ServiceIntentSender intentSender)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
queryRoots(String[] projection)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
queryDocument(String documentId, String[] projection)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
queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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
openDocument( String documentId, String mode, CancellationSignal signal)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
openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)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
deleteDocument(String documentId)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
onTrimMemory(int level)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
createDocument(String parentDocumentId, String mimeType, String displayName)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
findDocumentPath(String parentDocumentId, String childDocumentId)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
isChildDocument(String parentDocumentId, String documentId)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
getDocumentMetadata(String docId)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 
openDevice(int deviceId)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 
closeDevice(int deviceId)602     void closeDevice(int deviceId) throws IOException, InterruptedException {
603         synchronized (mDeviceListLock) {
604             closeDeviceInternal(deviceId);
605             mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
606         }
607         mRootScanner.resume();
608     }
609 
getOpenedDeviceRecordsCache()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      */
getDeviceDocumentId(int deviceId)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      */
resumeRootScanner()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
shutdown()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 
notifyChildDocumentsChange(String parentDocumentId)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      */
resume()676     private void resume() {
677         synchronized (mDeviceListLock) {
678             mDatabase.getMapper().clearMapping();
679         }
680     }
681 
closeDeviceInternal(int deviceId)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 
getDeviceToolkit(int deviceId)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 
getPipeManager(Identifier identifier)705     private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
706         return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
707     }
708 
getDocumentLoader(Identifier identifier)709     private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
710         return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
711     }
712 
getFileSize(String documentId)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      */
createErrorCursor(String[] projection, int stringResId)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 
DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database, MtpDeviceRecord record)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
close()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 
MtpProxyFileDescriptorCallback(int inode)771         MtpProxyFileDescriptorCallback(int inode) {
772             mInode = inode;
773         }
774 
775         @Override
onGetSize()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
onRead(long offset, int size, byte[] data)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
onWrite(long offset, int size, byte[] data)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
onFsync()823         public void onFsync() throws ErrnoException {
824             tryFsync();
825         }
826 
827         @Override
onRelease()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 
tryFsync()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