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
17package com.android.mtp;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.UriPermission;
23import android.content.res.AssetFileDescriptor;
24import android.content.res.Resources;
25import android.database.Cursor;
26import android.database.DatabaseUtils;
27import android.database.MatrixCursor;
28import android.database.sqlite.SQLiteDiskIOException;
29import android.graphics.Point;
30import android.media.MediaFile;
31import android.mtp.MtpConstants;
32import android.mtp.MtpObjectInfo;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.CancellationSignal;
36import android.os.FileUtils;
37import android.os.ParcelFileDescriptor;
38import android.os.ProxyFileDescriptorCallback;
39import android.os.storage.StorageManager;
40import android.provider.DocumentsContract.Document;
41import android.provider.DocumentsContract.Path;
42import android.provider.DocumentsContract.Root;
43import android.provider.DocumentsContract;
44import android.provider.DocumentsProvider;
45import android.provider.Settings;
46import android.system.ErrnoException;
47import android.system.OsConstants;
48import android.util.Log;
49
50import com.android.internal.annotations.GuardedBy;
51import com.android.internal.annotations.VisibleForTesting;
52
53import java.io.FileNotFoundException;
54import java.io.IOException;
55import java.util.HashMap;
56import java.util.LinkedList;
57import java.util.List;
58import java.util.Map;
59import java.util.concurrent.TimeoutException;
60import libcore.io.IoUtils;
61
62/**
63 * DocumentsProvider for MTP devices.
64 */
65public 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     */
99    static MtpDocumentsProvider getInstance() {
100        return sSingleton;
101    }
102
103    @Override
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
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
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
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
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
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
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
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
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
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
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
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
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
575    void closeDevice(int deviceId) throws IOException, InterruptedException {
576        synchronized (mDeviceListLock) {
577            closeDeviceInternal(deviceId);
578            mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
579        }
580        mRootScanner.resume();
581    }
582
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     */
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     */
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
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
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     */
649    private void resume() {
650        synchronized (mDeviceListLock) {
651            mDatabase.getMapper().clearMapping();
652        }
653    }
654
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
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
678    private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
679        return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
680    }
681
682    private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
683        return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
684    }
685
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     */
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
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
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
744        MtpProxyFileDescriptorCallback(int inode) {
745            mInode = inode;
746        }
747
748        @Override
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
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
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
796        public void onFsync() throws ErrnoException {
797            tryFsync();
798        }
799
800        @Override
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
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