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 353 final int parentObjectHandle; 354 final int storageId; 355 switch (parentId.mDocumentType) { 356 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE: 357 final String[] storageDocumentIds = 358 mDatabase.getStorageDocumentIds(parentId.mDocumentId); 359 if (storageDocumentIds.length == 1) { 360 final String newDocumentId = 361 createDocument(storageDocumentIds[0], mimeType, displayName); 362 notifyChildDocumentsChange(parentDocumentId); 363 return newDocumentId; 364 } else { 365 throw new UnsupportedOperationException( 366 "Cannot create a file under the device."); 367 } 368 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: 369 storageId = parentId.mStorageId; 370 parentObjectHandle = -1; 371 break; 372 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT: 373 storageId = parentId.mStorageId; 374 parentObjectHandle = parentId.mObjectHandle; 375 break; 376 default: 377 throw new IllegalArgumentException("Unexpected document type."); 378 } 379 380 pipe = ParcelFileDescriptor.createReliablePipe(); 381 int objectHandle = -1; 382 MtpObjectInfo info = null; 383 try { 384 pipe[0].close(); // 0 bytes for a new document. 385 386 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ? 387 MtpConstants.FORMAT_ASSOCIATION : 388 MediaFile.getFormatCode(displayName, mimeType); 389 info = new MtpObjectInfo.Builder() 390 .setStorageId(storageId) 391 .setParent(parentObjectHandle) 392 .setFormat(formatCode) 393 .setName(displayName) 394 .build(); 395 396 final String[] parts = FileUtils.splitFileName(mimeType, displayName); 397 final String baseName = parts[0]; 398 final String extension = parts[1]; 399 for (int i = 0; i <= 32; i++) { 400 final MtpObjectInfo infoUniqueName; 401 if (i == 0) { 402 infoUniqueName = info; 403 } else { 404 String suffixedName = baseName + " (" + i + " )"; 405 if (!extension.isEmpty()) { 406 suffixedName += "." + extension; 407 } 408 infoUniqueName = 409 new MtpObjectInfo.Builder(info).setName(suffixedName).build(); 410 } 411 try { 412 objectHandle = mMtpManager.createDocument( 413 parentId.mDeviceId, infoUniqueName, pipe[1]); 414 break; 415 } catch (SendObjectInfoFailure exp) { 416 // This can be caused when we have an existing file with the same name. 417 continue; 418 } 419 } 420 } finally { 421 pipe[1].close(); 422 } 423 if (objectHandle == -1) { 424 throw new IllegalArgumentException( 425 "The file name \"" + displayName + "\" is conflicted with existing files " + 426 "and the provider failed to find unique name."); 427 } 428 final MtpObjectInfo infoWithHandle = 429 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build(); 430 final String documentId = mDatabase.putNewDocument( 431 parentId.mDeviceId, parentDocumentId, record.operationsSupported, 432 infoWithHandle, 0l); 433 getDocumentLoader(parentId).cancelTask(parentId); 434 notifyChildDocumentsChange(parentDocumentId); 435 return documentId; 436 } catch (FileNotFoundException | RuntimeException error) { 437 Log.e(TAG, "createDocument", error); 438 throw error; 439 } catch (IOException error) { 440 Log.e(TAG, "createDocument", error); 441 throw new IllegalStateException(error); 442 } 443 } 444 445 @Override isChildDocument(String parentDocumentId, String documentId)446 public boolean isChildDocument(String parentDocumentId, String documentId) { 447 try { 448 Identifier identifier = mDatabase.createIdentifier(documentId); 449 while (true) { 450 if (parentDocumentId.equals(identifier.mDocumentId)) { 451 return true; 452 } 453 if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { 454 return false; 455 } 456 identifier = mDatabase.getParentIdentifier(identifier.mDocumentId); 457 } 458 } catch (FileNotFoundException error) { 459 return false; 460 } 461 } 462 openDevice(int deviceId)463 void openDevice(int deviceId) throws IOException { 464 synchronized (mDeviceListLock) { 465 if (mDeviceToolkits.containsKey(deviceId)) { 466 return; 467 } 468 if (DEBUG) { 469 Log.d(TAG, "Open device " + deviceId); 470 } 471 final MtpDeviceRecord device = mMtpManager.openDevice(deviceId); 472 final DeviceToolkit toolkit = 473 new DeviceToolkit(mMtpManager, mResolver, mDatabase, device); 474 mDeviceToolkits.put(deviceId, toolkit); 475 mIntentSender.sendUpdateNotificationIntent(); 476 try { 477 mRootScanner.resume().await(); 478 } catch (InterruptedException error) { 479 Log.e(TAG, "openDevice", error); 480 } 481 // Resume document loader to remap disconnected document ID. Must be invoked after the 482 // root scanner resumes. 483 toolkit.mDocumentLoader.resume(); 484 } 485 } 486 closeDevice(int deviceId)487 void closeDevice(int deviceId) throws IOException, InterruptedException { 488 synchronized (mDeviceListLock) { 489 closeDeviceInternal(deviceId); 490 } 491 mRootScanner.resume(); 492 mIntentSender.sendUpdateNotificationIntent(); 493 } 494 getOpenedDeviceRecordsCache()495 MtpDeviceRecord[] getOpenedDeviceRecordsCache() { 496 synchronized (mDeviceListLock) { 497 final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()]; 498 int i = 0; 499 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { 500 records[i] = toolkit.mDeviceRecord; 501 i++; 502 } 503 return records; 504 } 505 } 506 507 /** 508 * Obtains document ID for the given device ID. 509 * @param deviceId 510 * @return document ID 511 * @throws FileNotFoundException device ID has not been build. 512 */ getDeviceDocumentId(int deviceId)513 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException { 514 return mDatabase.getDeviceDocumentId(deviceId); 515 } 516 517 /** 518 * Resumes root scanner to handle the update of device list. 519 */ resumeRootScanner()520 void resumeRootScanner() { 521 if (DEBUG) { 522 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner"); 523 } 524 mRootScanner.resume(); 525 } 526 527 /** 528 * Finalize the content provider for unit tests. 529 */ 530 @Override shutdown()531 public void shutdown() { 532 synchronized (mDeviceListLock) { 533 try { 534 // Copy the opened key set because it will be modified when closing devices. 535 final Integer[] keySet = 536 mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]); 537 for (final int id : keySet) { 538 closeDeviceInternal(id); 539 } 540 mRootScanner.pause(); 541 } catch (InterruptedException | IOException | TimeoutException e) { 542 // It should fail unit tests by throwing runtime exception. 543 throw new RuntimeException(e); 544 } finally { 545 mDatabase.close(); 546 mAppFuse.close(); 547 super.shutdown(); 548 } 549 } 550 } 551 notifyChildDocumentsChange(String parentDocumentId)552 private void notifyChildDocumentsChange(String parentDocumentId) { 553 mResolver.notifyChange( 554 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId), 555 null, 556 false); 557 } 558 559 /** 560 * Clears MTP identifier in the database. 561 */ resume()562 private void resume() { 563 synchronized (mDeviceListLock) { 564 mDatabase.getMapper().clearMapping(); 565 } 566 } 567 closeDeviceInternal(int deviceId)568 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException { 569 // TODO: Flush the device before closing (if not closed externally). 570 if (!mDeviceToolkits.containsKey(deviceId)) { 571 return; 572 } 573 if (DEBUG) { 574 Log.d(TAG, "Close device " + deviceId); 575 } 576 getDeviceToolkit(deviceId).close(); 577 mDeviceToolkits.remove(deviceId); 578 mMtpManager.closeDevice(deviceId); 579 } 580 getDeviceToolkit(int deviceId)581 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException { 582 synchronized (mDeviceListLock) { 583 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId); 584 if (toolkit == null) { 585 throw new FileNotFoundException(); 586 } 587 return toolkit; 588 } 589 } 590 getPipeManager(Identifier identifier)591 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { 592 return getDeviceToolkit(identifier.mDeviceId).mPipeManager; 593 } 594 getDocumentLoader(Identifier identifier)595 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { 596 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; 597 } 598 getFileSize(String documentId)599 private long getFileSize(String documentId) throws FileNotFoundException { 600 final Cursor cursor = mDatabase.queryDocument( 601 documentId, 602 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME)); 603 try { 604 if (cursor.moveToNext()) { 605 if (cursor.isNull(0)) { 606 throw new UnsupportedOperationException(); 607 } 608 return cursor.getLong(0); 609 } else { 610 throw new FileNotFoundException(); 611 } 612 } finally { 613 cursor.close(); 614 } 615 } 616 617 /** 618 * Creates empty cursor with specific error message. 619 * 620 * @param projection Column names. 621 * @param stringResId String resource ID of error message. 622 * @return Empty cursor with error message. 623 */ createErrorCursor(String[] projection, int stringResId)624 private Cursor createErrorCursor(String[] projection, int stringResId) { 625 final Bundle bundle = new Bundle(); 626 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId)); 627 final Cursor cursor = new MatrixCursor(projection); 628 cursor.setExtras(bundle); 629 return cursor; 630 } 631 632 private static class DeviceToolkit implements AutoCloseable { 633 public final PipeManager mPipeManager; 634 public final DocumentLoader mDocumentLoader; 635 public final MtpDeviceRecord mDeviceRecord; 636 DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database, MtpDeviceRecord record)637 public DeviceToolkit(MtpManager manager, 638 ContentResolver resolver, 639 MtpDatabase database, 640 MtpDeviceRecord record) { 641 mPipeManager = new PipeManager(database); 642 mDocumentLoader = new DocumentLoader(record, manager, resolver, database); 643 mDeviceRecord = record; 644 } 645 646 @Override close()647 public void close() throws InterruptedException { 648 mPipeManager.close(); 649 mDocumentLoader.close(); 650 } 651 } 652 653 private class AppFuseCallback implements AppFuse.Callback { 654 private final Map<Long, MtpFileWriter> mWriters = new HashMap<>(); 655 656 @Override getFileSize(int inode)657 public long getFileSize(int inode) throws FileNotFoundException { 658 return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode)); 659 } 660 661 @Override readObjectBytes( int inode, long offset, long size, byte[] buffer)662 public long readObjectBytes( 663 int inode, long offset, long size, byte[] buffer) throws IOException { 664 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode)); 665 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord; 666 667 if (MtpDeviceRecord.isSupported( 668 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) { 669 return mMtpManager.getPartialObject64( 670 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer); 671 } 672 673 if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported( 674 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) { 675 return mMtpManager.getPartialObject( 676 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer); 677 } 678 679 throw new UnsupportedOperationException(); 680 } 681 682 @Override writeObjectBytes( long fileHandle, int inode, long offset, int size, byte[] bytes)683 public int writeObjectBytes( 684 long fileHandle, int inode, long offset, int size, byte[] bytes) 685 throws IOException, ErrnoException { 686 final MtpFileWriter writer; 687 if (mWriters.containsKey(fileHandle)) { 688 writer = mWriters.get(fileHandle); 689 } else { 690 writer = new MtpFileWriter(mContext, String.valueOf(inode)); 691 mWriters.put(fileHandle, writer); 692 } 693 return writer.write(offset, size, bytes); 694 } 695 696 @Override flushFileHandle(long fileHandle)697 public void flushFileHandle(long fileHandle) throws IOException, ErrnoException { 698 final MtpFileWriter writer = mWriters.get(fileHandle); 699 if (writer == null) { 700 // File handle for reading. 701 return; 702 } 703 final MtpDeviceRecord device = getDeviceToolkit( 704 mDatabase.createIdentifier(writer.getDocumentId()).mDeviceId).mDeviceRecord; 705 writer.flush(mMtpManager, mDatabase, device.operationsSupported); 706 } 707 708 @Override closeFileHandle(long fileHandle)709 public void closeFileHandle(long fileHandle) throws IOException, ErrnoException { 710 final MtpFileWriter writer = mWriters.get(fileHandle); 711 if (writer == null) { 712 // File handle for reading. 713 return; 714 } 715 try { 716 writer.close(); 717 } finally { 718 mWriters.remove(fileHandle); 719 } 720 } 721 } 722 } 723