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.Context;
21 import android.content.UriPermission;
22 import android.content.res.AssetFileDescriptor;
23 import android.content.res.Resources;
24 import android.database.Cursor;
25 import android.database.MatrixCursor;
26 import android.database.sqlite.SQLiteDiskIOException;
27 import android.graphics.Point;
28 import android.media.MediaFile;
29 import android.mtp.MtpConstants;
30 import android.mtp.MtpObjectInfo;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.CancellationSignal;
34 import android.os.FileUtils;
35 import android.os.ParcelFileDescriptor;
36 import android.os.storage.StorageManager;
37 import android.provider.DocumentsContract.Document;
38 import android.provider.DocumentsContract.Root;
39 import android.provider.DocumentsContract;
40 import android.provider.DocumentsProvider;
41 import android.provider.Settings;
42 import android.system.ErrnoException;
43 import android.system.Os;
44 import android.system.OsConstants;
45 import android.util.Log;
46 
47 import com.android.internal.annotations.GuardedBy;
48 import com.android.internal.annotations.VisibleForTesting;
49 
50 import java.io.File;
51 import java.io.FileDescriptor;
52 import java.io.FileNotFoundException;
53 import java.io.IOException;
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.concurrent.TimeoutException;
58 
59 /**
60  * DocumentsProvider for MTP devices.
61  */
62 public 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      */
getInstance()96     static MtpDocumentsProvider getInstance() {
97         return sSingleton;
98     }
99 
100     @Override
onCreate()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
onCreateForTesting( Context context, Resources resources, MtpManager mtpManager, ContentResolver resolver, MtpDatabase database, StorageManager storageManager, ServiceIntentSender intentSender)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
queryRoots(String[] projection)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
queryDocument(String documentId, String[] projection)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
queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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
openDocument( String documentId, String mode, CancellationSignal signal)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
openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)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
deleteDocument(String documentId)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
onTrimMemory(int level)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
createDocument(String parentDocumentId, String mimeType, String displayName)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 
openDevice(int deviceId)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 
closeDevice(int deviceId)441     void closeDevice(int deviceId) throws IOException, InterruptedException {
442         synchronized (mDeviceListLock) {
443             closeDeviceInternal(deviceId);
444         }
445         mRootScanner.resume();
446         mIntentSender.sendUpdateNotificationIntent();
447     }
448 
getOpenedDeviceRecordsCache()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      */
getDeviceDocumentId(int deviceId)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      */
resumeRootScanner()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
shutdown()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 
notifyChildDocumentsChange(String parentDocumentId)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      */
resume()516     private void resume() {
517         synchronized (mDeviceListLock) {
518             mDatabase.getMapper().clearMapping();
519         }
520     }
521 
closeDeviceInternal(int deviceId)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 
getDeviceToolkit(int deviceId)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 
getPipeManager(Identifier identifier)545     private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
546         return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
547     }
548 
getDocumentLoader(Identifier identifier)549     private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
550         return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
551     }
552 
getFileSize(String documentId)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      */
createErrorCursor(String[] projection, int stringResId)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 
DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database, MtpDeviceRecord record)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
close()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
getFileSize(int inode)611         public long getFileSize(int inode) throws FileNotFoundException {
612             return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
613         }
614 
615         @Override
readObjectBytes( int inode, long offset, long size, byte[] buffer)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
writeObjectBytes( long fileHandle, int inode, long offset, int size, byte[] bytes)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
flushFileHandle(long fileHandle)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
closeFileHandle(long fileHandle)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