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