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.annotation.Nullable; 20 import android.content.ContentResolver; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.UriPermission; 24 import android.content.res.AssetFileDescriptor; 25 import android.content.res.Resources; 26 import android.database.Cursor; 27 import android.database.DatabaseUtils; 28 import android.database.MatrixCursor; 29 import android.database.sqlite.SQLiteDiskIOException; 30 import android.graphics.Point; 31 import android.media.MediaFile; 32 import android.mtp.MtpConstants; 33 import android.mtp.MtpObjectInfo; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.CancellationSignal; 37 import android.os.FileUtils; 38 import android.os.ParcelFileDescriptor; 39 import android.os.ProxyFileDescriptorCallback; 40 import android.os.storage.StorageManager; 41 import android.provider.DocumentsContract; 42 import android.provider.DocumentsContract.Document; 43 import android.provider.DocumentsContract.Path; 44 import android.provider.DocumentsContract.Root; 45 import android.provider.DocumentsProvider; 46 import android.provider.MetadataReader; 47 import android.provider.Settings; 48 import android.system.ErrnoException; 49 import android.system.OsConstants; 50 import android.util.Log; 51 52 import com.android.internal.annotations.GuardedBy; 53 import com.android.internal.annotations.VisibleForTesting; 54 55 import libcore.io.IoUtils; 56 57 import java.io.FileNotFoundException; 58 import java.io.IOException; 59 import java.io.InputStream; 60 import java.util.HashMap; 61 import java.util.LinkedList; 62 import java.util.List; 63 import java.util.Map; 64 import java.util.concurrent.TimeoutException; 65 66 /** 67 * DocumentsProvider for MTP devices. 68 */ 69 public class MtpDocumentsProvider extends DocumentsProvider { 70 static final String AUTHORITY = "com.android.mtp.documents"; 71 static final String TAG = "MtpDocumentsProvider"; 72 static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 73 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 74 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, 75 Root.COLUMN_AVAILABLE_BYTES, 76 }; 77 static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 78 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, 79 Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, 80 Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 81 }; 82 83 static final boolean DEBUG = false; 84 85 private final Object mDeviceListLock = new Object(); 86 87 private static MtpDocumentsProvider sSingleton; 88 89 private MtpManager mMtpManager; 90 private ContentResolver mResolver; 91 @GuardedBy("mDeviceListLock") 92 private Map<Integer, DeviceToolkit> mDeviceToolkits; 93 private RootScanner mRootScanner; 94 private Resources mResources; 95 private MtpDatabase mDatabase; 96 private ServiceIntentSender mIntentSender; 97 private Context mContext; 98 private StorageManager mStorageManager; 99 100 /** 101 * Provides singleton instance to MtpDocumentsService. 102 */ 103 static MtpDocumentsProvider getInstance() { 104 return sSingleton; 105 } 106 107 @Override 108 public boolean onCreate() { 109 sSingleton = this; 110 mContext = getContext(); 111 mResources = getContext().getResources(); 112 mMtpManager = new MtpManager(getContext()); 113 mResolver = getContext().getContentResolver(); 114 mDeviceToolkits = new HashMap<>(); 115 mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE); 116 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase); 117 mIntentSender = new ServiceIntentSender(getContext()); 118 mStorageManager = getContext().getSystemService(StorageManager.class); 119 120 // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider 121 // after booting. 122 try { 123 final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1); 124 final int lastBootCount = mDatabase.getLastBootCount(); 125 if (bootCount != -1 && bootCount != lastBootCount) { 126 mDatabase.setLastBootCount(bootCount); 127 final List<UriPermission> permissions = 128 mResolver.getOutgoingPersistedUriPermissions(); 129 final Uri[] uris = new Uri[permissions.size()]; 130 for (int i = 0; i < permissions.size(); i++) { 131 uris[i] = permissions.get(i).getUri(); 132 } 133 mDatabase.cleanDatabase(uris); 134 } 135 } catch (SQLiteDiskIOException error) { 136 // It can happen due to disk shortage. 137 Log.e(TAG, "Failed to clean database.", error); 138 return false; 139 } 140 141 resume(); 142 return true; 143 } 144 145 @VisibleForTesting 146 boolean onCreateForTesting( 147 Context context, 148 Resources resources, 149 MtpManager mtpManager, 150 ContentResolver resolver, 151 MtpDatabase database, 152 StorageManager storageManager, 153 ServiceIntentSender intentSender) { 154 mContext = context; 155 mResources = resources; 156 mMtpManager = mtpManager; 157 mResolver = resolver; 158 mDeviceToolkits = new HashMap<>(); 159 mDatabase = database; 160 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase); 161 mIntentSender = intentSender; 162 mStorageManager = storageManager; 163 164 resume(); 165 return true; 166 } 167 168 @Override 169 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 170 if (projection == null) { 171 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION; 172 } 173 final Cursor cursor = mDatabase.queryRoots(mResources, projection); 174 cursor.setNotificationUri( 175 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY)); 176 return cursor; 177 } 178 179 @Override 180 public Cursor queryDocument(String documentId, String[] projection) 181 throws FileNotFoundException { 182 if (projection == null) { 183 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 184 } 185 final Cursor cursor = mDatabase.queryDocument(documentId, projection); 186 final int cursorCount = cursor.getCount(); 187 if (cursorCount == 0) { 188 cursor.close(); 189 throw new FileNotFoundException(); 190 } else if (cursorCount != 1) { 191 cursor.close(); 192 Log.wtf(TAG, "Unexpected cursor size: " + cursorCount); 193 return null; 194 } 195 196 final Identifier identifier = mDatabase.createIdentifier(documentId); 197 if (identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { 198 return cursor; 199 } 200 final String[] storageDocIds = mDatabase.getStorageDocumentIds(documentId); 201 if (storageDocIds.length != 1) { 202 return mDatabase.queryDocument(documentId, projection); 203 } 204 205 // If the documentId specifies a device having exact one storage, we repalce some device 206 // attributes with the storage attributes. 207 try { 208 final String storageName; 209 final int storageFlags; 210 try (final Cursor storageCursor = mDatabase.queryDocument( 211 storageDocIds[0], 212 MtpDatabase.strings(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS))) { 213 if (!storageCursor.moveToNext()) { 214 throw new FileNotFoundException(); 215 } 216 storageName = storageCursor.getString(0); 217 storageFlags = storageCursor.getInt(1); 218 } 219 220 cursor.moveToNext(); 221 final ContentValues values = new ContentValues(); 222 DatabaseUtils.cursorRowToContentValues(cursor, values); 223 if (values.containsKey(Document.COLUMN_DISPLAY_NAME)) { 224 values.put(Document.COLUMN_DISPLAY_NAME, mResources.getString( 225 R.string.root_name, 226 values.getAsString(Document.COLUMN_DISPLAY_NAME), 227 storageName)); 228 } 229 values.put(Document.COLUMN_FLAGS, storageFlags); 230 final MatrixCursor output = new MatrixCursor(projection, 1); 231 MtpDatabase.putValuesToCursor(values, output); 232 return output; 233 } finally { 234 cursor.close(); 235 } 236 } 237 238 @Override 239 public Cursor queryChildDocuments(String parentDocumentId, 240 String[] projection, String sortOrder) throws FileNotFoundException { 241 if (DEBUG) { 242 Log.d(TAG, "queryChildDocuments: " + parentDocumentId); 243 } 244 if (projection == null) { 245 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 246 } 247 Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId); 248 try { 249 openDevice(parentIdentifier.mDeviceId); 250 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { 251 final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId); 252 if (storageDocIds.length == 0) { 253 // Remote device does not provide storages. Maybe it is locked. 254 return createErrorCursor(projection, R.string.error_locked_device); 255 } else if (storageDocIds.length > 1) { 256 // Returns storage list from database. 257 return mDatabase.queryChildDocuments(projection, parentDocumentId); 258 } 259 260 // Exact one storage is found. Skip storage and returns object in the single 261 // storage. 262 parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]); 263 } 264 265 // Returns object list from document loader. 266 return getDocumentLoader(parentIdentifier).queryChildDocuments( 267 projection, parentIdentifier); 268 } catch (BusyDeviceException exception) { 269 return createErrorCursor(projection, R.string.error_busy_device); 270 } catch (IOException exception) { 271 Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception); 272 throw new FileNotFoundException(exception.getMessage()); 273 } 274 } 275 276 @Override 277 public ParcelFileDescriptor openDocument( 278 String documentId, String mode, CancellationSignal signal) 279 throws FileNotFoundException { 280 if (DEBUG) { 281 Log.d(TAG, "openDocument: " + documentId); 282 } 283 final Identifier identifier = mDatabase.createIdentifier(documentId); 284 try { 285 openDevice(identifier.mDeviceId); 286 final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord; 287 // Turn off MODE_CREATE because openDocument does not allow to create new files. 288 final int modeFlag = 289 ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE; 290 if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) { 291 long fileSize; 292 try { 293 fileSize = getFileSize(documentId); 294 } catch (UnsupportedOperationException exception) { 295 fileSize = -1; 296 } 297 if (MtpDeviceRecord.isPartialReadSupported( 298 device.operationsSupported, fileSize)) { 299 300 return mStorageManager.openProxyFileDescriptor( 301 modeFlag, 302 new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId))); 303 } else { 304 // If getPartialObject{|64} are not supported for the device, returns 305 // non-seekable pipe FD instead. 306 return getPipeManager(identifier).readDocument(mMtpManager, identifier); 307 } 308 } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) { 309 // TODO: Clear the parent document loader task (if exists) and call notify 310 // when writing is completed. 311 if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) { 312 return mStorageManager.openProxyFileDescriptor( 313 modeFlag, 314 new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId))); 315 } else { 316 throw new UnsupportedOperationException( 317 "The device does not support writing operation."); 318 } 319 } else { 320 // TODO: Add support for "rw" mode. 321 throw new UnsupportedOperationException("The provider does not support 'rw' mode."); 322 } 323 } catch (FileNotFoundException | RuntimeException error) { 324 Log.e(MtpDocumentsProvider.TAG, "openDocument", error); 325 throw error; 326 } catch (IOException error) { 327 Log.e(MtpDocumentsProvider.TAG, "openDocument", error); 328 throw new IllegalStateException(error); 329 } 330 } 331 332 @Override 333 public AssetFileDescriptor openDocumentThumbnail( 334 String documentId, 335 Point sizeHint, 336 CancellationSignal signal) throws FileNotFoundException { 337 final Identifier identifier = mDatabase.createIdentifier(documentId); 338 try { 339 openDevice(identifier.mDeviceId); 340 return new AssetFileDescriptor( 341 getPipeManager(identifier).readThumbnail(mMtpManager, identifier), 342 0, // Start offset. 343 AssetFileDescriptor.UNKNOWN_LENGTH); 344 } catch (IOException error) { 345 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error); 346 throw new FileNotFoundException(error.getMessage()); 347 } 348 } 349 350 @Override 351 public void deleteDocument(String documentId) throws FileNotFoundException { 352 try { 353 final Identifier identifier = mDatabase.createIdentifier(documentId); 354 openDevice(identifier.mDeviceId); 355 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId); 356 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle); 357 mDatabase.deleteDocument(documentId); 358 getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier); 359 notifyChildDocumentsChange(parentIdentifier.mDocumentId); 360 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) { 361 // If the parent is storage, the object might be appeared as child of device because 362 // we skip storage when the device has only one storage. 363 final Identifier deviceIdentifier = mDatabase.getParentIdentifier( 364 parentIdentifier.mDocumentId); 365 notifyChildDocumentsChange(deviceIdentifier.mDocumentId); 366 } 367 } catch (IOException error) { 368 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error); 369 throw new FileNotFoundException(error.getMessage()); 370 } 371 } 372 373 @Override 374 public void onTrimMemory(int level) { 375 synchronized (mDeviceListLock) { 376 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { 377 toolkit.mDocumentLoader.clearCompletedTasks(); 378 } 379 } 380 } 381 382 @Override 383 public String createDocument(String parentDocumentId, String mimeType, String displayName) 384 throws FileNotFoundException { 385 if (DEBUG) { 386 Log.d(TAG, "createDocument: " + displayName); 387 } 388 final Identifier parentId; 389 final MtpDeviceRecord record; 390 final ParcelFileDescriptor[] pipe; 391 try { 392 parentId = mDatabase.createIdentifier(parentDocumentId); 393 openDevice(parentId.mDeviceId); 394 record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord; 395 if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) { 396 throw new UnsupportedOperationException( 397 "Writing operation is not supported by the device."); 398 } 399 400 final int parentObjectHandle; 401 final int storageId; 402 switch (parentId.mDocumentType) { 403 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE: 404 final String[] storageDocumentIds = 405 mDatabase.getStorageDocumentIds(parentId.mDocumentId); 406 if (storageDocumentIds.length == 1) { 407 final String newDocumentId = 408 createDocument(storageDocumentIds[0], mimeType, displayName); 409 notifyChildDocumentsChange(parentDocumentId); 410 return newDocumentId; 411 } else { 412 throw new UnsupportedOperationException( 413 "Cannot create a file under the device."); 414 } 415 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: 416 storageId = parentId.mStorageId; 417 parentObjectHandle = -1; 418 break; 419 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT: 420 storageId = parentId.mStorageId; 421 parentObjectHandle = parentId.mObjectHandle; 422 break; 423 default: 424 throw new IllegalArgumentException("Unexpected document type."); 425 } 426 427 pipe = ParcelFileDescriptor.createReliablePipe(); 428 int objectHandle = -1; 429 MtpObjectInfo info = null; 430 try { 431 pipe[0].close(); // 0 bytes for a new document. 432 433 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ? 434 MtpConstants.FORMAT_ASSOCIATION : 435 MediaFile.getFormatCode(displayName, mimeType); 436 info = new MtpObjectInfo.Builder() 437 .setStorageId(storageId) 438 .setParent(parentObjectHandle) 439 .setFormat(formatCode) 440 .setName(displayName) 441 .build(); 442 443 final String[] parts = FileUtils.splitFileName(mimeType, displayName); 444 final String baseName = parts[0]; 445 final String extension = parts[1]; 446 for (int i = 0; i <= 32; i++) { 447 final MtpObjectInfo infoUniqueName; 448 if (i == 0) { 449 infoUniqueName = info; 450 } else { 451 String suffixedName = baseName + " (" + i + " )"; 452 if (!extension.isEmpty()) { 453 suffixedName += "." + extension; 454 } 455 infoUniqueName = 456 new MtpObjectInfo.Builder(info).setName(suffixedName).build(); 457 } 458 try { 459 objectHandle = mMtpManager.createDocument( 460 parentId.mDeviceId, infoUniqueName, pipe[1]); 461 break; 462 } catch (SendObjectInfoFailure exp) { 463 // This can be caused when we have an existing file with the same name. 464 continue; 465 } 466 } 467 } finally { 468 pipe[1].close(); 469 } 470 if (objectHandle == -1) { 471 throw new IllegalArgumentException( 472 "The file name \"" + displayName + "\" is conflicted with existing files " + 473 "and the provider failed to find unique name."); 474 } 475 final MtpObjectInfo infoWithHandle = 476 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build(); 477 final String documentId = mDatabase.putNewDocument( 478 parentId.mDeviceId, parentDocumentId, record.operationsSupported, 479 infoWithHandle, 0l); 480 getDocumentLoader(parentId).cancelTask(parentId); 481 notifyChildDocumentsChange(parentDocumentId); 482 return documentId; 483 } catch (FileNotFoundException | RuntimeException error) { 484 Log.e(TAG, "createDocument", error); 485 throw error; 486 } catch (IOException error) { 487 Log.e(TAG, "createDocument", error); 488 throw new IllegalStateException(error); 489 } 490 } 491 492 @Override 493 public Path findDocumentPath(String parentDocumentId, String childDocumentId) 494 throws FileNotFoundException { 495 final LinkedList<String> ids = new LinkedList<>(); 496 final Identifier childIdentifier = mDatabase.createIdentifier(childDocumentId); 497 498 Identifier i = childIdentifier; 499 outer: while (true) { 500 if (i.mDocumentId.equals(parentDocumentId)) { 501 ids.addFirst(i.mDocumentId); 502 break; 503 } 504 switch (i.mDocumentType) { 505 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT: 506 ids.addFirst(i.mDocumentId); 507 i = mDatabase.getParentIdentifier(i.mDocumentId); 508 break; 509 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: { 510 // Check if there is the multiple storage. 511 final Identifier deviceIdentifier = 512 mDatabase.getParentIdentifier(i.mDocumentId); 513 final String[] storageIds = 514 mDatabase.getStorageDocumentIds(deviceIdentifier.mDocumentId); 515 // Add storage's document ID to the path only when the device has multiple 516 // storages. 517 if (storageIds.length > 1) { 518 ids.addFirst(i.mDocumentId); 519 break outer; 520 } 521 i = deviceIdentifier; 522 break; 523 } 524 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE: 525 ids.addFirst(i.mDocumentId); 526 break outer; 527 } 528 } 529 530 if (parentDocumentId != null) { 531 return new Path(null, ids); 532 } else { 533 return new Path(/* Should be same with root ID */ i.mDocumentId, ids); 534 } 535 } 536 537 @Override 538 public boolean isChildDocument(String parentDocumentId, String documentId) { 539 try { 540 Identifier identifier = mDatabase.createIdentifier(documentId); 541 while (true) { 542 if (parentDocumentId.equals(identifier.mDocumentId)) { 543 return true; 544 } 545 if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { 546 return false; 547 } 548 identifier = mDatabase.getParentIdentifier(identifier.mDocumentId); 549 } 550 } catch (FileNotFoundException error) { 551 return false; 552 } 553 } 554 555 @Override 556 public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException { 557 String mimeType = getDocumentType(docId); 558 559 if (!MetadataReader.isSupportedMimeType(mimeType)) { 560 return null; 561 } 562 563 InputStream stream = null; 564 try { 565 stream = new ParcelFileDescriptor.AutoCloseInputStream( 566 openDocument(docId, "r", null)); 567 Bundle metadata = new Bundle(); 568 MetadataReader.getMetadata(metadata, stream, mimeType, null); 569 return metadata; 570 } catch (IOException e) { 571 Log.e(TAG, "An error occurred retrieving the metadata", e); 572 return null; 573 } finally { 574 IoUtils.closeQuietly(stream); 575 } 576 } 577 578 void openDevice(int deviceId) throws IOException { 579 synchronized (mDeviceListLock) { 580 if (mDeviceToolkits.containsKey(deviceId)) { 581 return; 582 } 583 if (DEBUG) { 584 Log.d(TAG, "Open device " + deviceId); 585 } 586 final MtpDeviceRecord device = mMtpManager.openDevice(deviceId); 587 final DeviceToolkit toolkit = 588 new DeviceToolkit(mMtpManager, mResolver, mDatabase, device); 589 mDeviceToolkits.put(deviceId, toolkit); 590 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache()); 591 try { 592 mRootScanner.resume().await(); 593 } catch (InterruptedException error) { 594 Log.e(TAG, "openDevice", error); 595 } 596 // Resume document loader to remap disconnected document ID. Must be invoked after the 597 // root scanner resumes. 598 toolkit.mDocumentLoader.resume(); 599 } 600 } 601 602 void closeDevice(int deviceId) throws IOException, InterruptedException { 603 synchronized (mDeviceListLock) { 604 closeDeviceInternal(deviceId); 605 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache()); 606 } 607 mRootScanner.resume(); 608 } 609 610 MtpDeviceRecord[] getOpenedDeviceRecordsCache() { 611 synchronized (mDeviceListLock) { 612 final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()]; 613 int i = 0; 614 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { 615 records[i] = toolkit.mDeviceRecord; 616 i++; 617 } 618 return records; 619 } 620 } 621 622 /** 623 * Obtains document ID for the given device ID. 624 * @param deviceId 625 * @return document ID 626 * @throws FileNotFoundException device ID has not been build. 627 */ 628 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException { 629 return mDatabase.getDeviceDocumentId(deviceId); 630 } 631 632 /** 633 * Resumes root scanner to handle the update of device list. 634 */ 635 void resumeRootScanner() { 636 if (DEBUG) { 637 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner"); 638 } 639 mRootScanner.resume(); 640 } 641 642 /** 643 * Finalize the content provider for unit tests. 644 */ 645 @Override 646 public void shutdown() { 647 synchronized (mDeviceListLock) { 648 try { 649 // Copy the opened key set because it will be modified when closing devices. 650 final Integer[] keySet = 651 mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]); 652 for (final int id : keySet) { 653 closeDeviceInternal(id); 654 } 655 mRootScanner.pause(); 656 } catch (InterruptedException | IOException | TimeoutException e) { 657 // It should fail unit tests by throwing runtime exception. 658 throw new RuntimeException(e); 659 } finally { 660 mDatabase.close(); 661 super.shutdown(); 662 } 663 } 664 } 665 666 private void notifyChildDocumentsChange(String parentDocumentId) { 667 mResolver.notifyChange( 668 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId), 669 null, 670 false); 671 } 672 673 /** 674 * Clears MTP identifier in the database. 675 */ 676 private void resume() { 677 synchronized (mDeviceListLock) { 678 mDatabase.getMapper().clearMapping(); 679 } 680 } 681 682 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException { 683 // TODO: Flush the device before closing (if not closed externally). 684 if (!mDeviceToolkits.containsKey(deviceId)) { 685 return; 686 } 687 if (DEBUG) { 688 Log.d(TAG, "Close device " + deviceId); 689 } 690 getDeviceToolkit(deviceId).close(); 691 mDeviceToolkits.remove(deviceId); 692 mMtpManager.closeDevice(deviceId); 693 } 694 695 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException { 696 synchronized (mDeviceListLock) { 697 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId); 698 if (toolkit == null) { 699 throw new FileNotFoundException(); 700 } 701 return toolkit; 702 } 703 } 704 705 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { 706 return getDeviceToolkit(identifier.mDeviceId).mPipeManager; 707 } 708 709 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { 710 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; 711 } 712 713 private long getFileSize(String documentId) throws FileNotFoundException { 714 final Cursor cursor = mDatabase.queryDocument( 715 documentId, 716 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME)); 717 try { 718 if (cursor.moveToNext()) { 719 if (cursor.isNull(0)) { 720 throw new UnsupportedOperationException(); 721 } 722 return cursor.getLong(0); 723 } else { 724 throw new FileNotFoundException(); 725 } 726 } finally { 727 cursor.close(); 728 } 729 } 730 731 /** 732 * Creates empty cursor with specific error message. 733 * 734 * @param projection Column names. 735 * @param stringResId String resource ID of error message. 736 * @return Empty cursor with error message. 737 */ 738 private Cursor createErrorCursor(String[] projection, int stringResId) { 739 final Bundle bundle = new Bundle(); 740 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId)); 741 final Cursor cursor = new MatrixCursor(projection); 742 cursor.setExtras(bundle); 743 return cursor; 744 } 745 746 private static class DeviceToolkit implements AutoCloseable { 747 public final PipeManager mPipeManager; 748 public final DocumentLoader mDocumentLoader; 749 public final MtpDeviceRecord mDeviceRecord; 750 751 public DeviceToolkit(MtpManager manager, 752 ContentResolver resolver, 753 MtpDatabase database, 754 MtpDeviceRecord record) { 755 mPipeManager = new PipeManager(database); 756 mDocumentLoader = new DocumentLoader(record, manager, resolver, database); 757 mDeviceRecord = record; 758 } 759 760 @Override 761 public void close() throws InterruptedException { 762 mPipeManager.close(); 763 mDocumentLoader.close(); 764 } 765 } 766 767 private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback { 768 private final int mInode; 769 private MtpFileWriter mWriter; 770 771 MtpProxyFileDescriptorCallback(int inode) { 772 mInode = inode; 773 } 774 775 @Override 776 public long onGetSize() throws ErrnoException { 777 try { 778 return getFileSize(String.valueOf(mInode)); 779 } catch (FileNotFoundException e) { 780 Log.e(TAG, e.getMessage(), e); 781 throw new ErrnoException("onGetSize", OsConstants.ENOENT); 782 } 783 } 784 785 @Override 786 public int onRead(long offset, int size, byte[] data) throws ErrnoException { 787 try { 788 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(mInode)); 789 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord; 790 if (MtpDeviceRecord.isSupported( 791 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) { 792 793 return (int) mMtpManager.getPartialObject64( 794 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data); 795 796 } 797 if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported( 798 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) { 799 return (int) mMtpManager.getPartialObject( 800 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data); 801 } 802 throw new ErrnoException("onRead", OsConstants.ENOTSUP); 803 } catch (IOException e) { 804 Log.e(TAG, e.getMessage(), e); 805 throw new ErrnoException("onRead", OsConstants.EIO); 806 } 807 } 808 809 @Override 810 public int onWrite(long offset, int size, byte[] data) throws ErrnoException { 811 try { 812 if (mWriter == null) { 813 mWriter = new MtpFileWriter(mContext, String.valueOf(mInode)); 814 } 815 return mWriter.write(offset, size, data); 816 } catch (IOException e) { 817 Log.e(TAG, e.getMessage(), e); 818 throw new ErrnoException("onWrite", OsConstants.EIO); 819 } 820 } 821 822 @Override 823 public void onFsync() throws ErrnoException { 824 tryFsync(); 825 } 826 827 @Override 828 public void onRelease() { 829 try { 830 tryFsync(); 831 } catch (ErrnoException error) { 832 // Cannot recover from the error at onRelease. Client app should use fsync to 833 // ensure the provider writes data correctly. 834 Log.e(TAG, "Cannot recover from the error at onRelease.", error); 835 } finally { 836 if (mWriter != null) { 837 IoUtils.closeQuietly(mWriter); 838 } 839 } 840 } 841 842 private void tryFsync() throws ErrnoException { 843 try { 844 if (mWriter != null) { 845 final MtpDeviceRecord device = 846 getDeviceToolkit(mDatabase.createIdentifier( 847 mWriter.getDocumentId()).mDeviceId).mDeviceRecord; 848 mWriter.flush(mMtpManager, mDatabase, device.operationsSupported); 849 } 850 } catch (IOException e) { 851 Log.e(TAG, e.getMessage(), e); 852 throw new ErrnoException("onWrite", OsConstants.EIO); 853 } 854 } 855 } 856 } 857