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