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