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