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