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 */ getInstance()103 static MtpDocumentsProvider getInstance() { 104 return sSingleton; 105 } 106 107 @Override onCreate()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 onCreateForTesting( Context context, Resources resources, MtpManager mtpManager, ContentResolver resolver, MtpDatabase database, StorageManager storageManager, ServiceIntentSender intentSender)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 queryRoots(String[] projection)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 queryDocument(String documentId, String[] projection)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 queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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 openDocument( String documentId, String mode, CancellationSignal signal)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 openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)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 deleteDocument(String documentId)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 onTrimMemory(int level)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 createDocument(String parentDocumentId, String mimeType, String displayName)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 findDocumentPath(String parentDocumentId, String childDocumentId)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 isChildDocument(String parentDocumentId, String documentId)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 getDocumentMetadata(String docId)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 openDevice(int deviceId)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 closeDevice(int deviceId)602 void closeDevice(int deviceId) throws IOException, InterruptedException { 603 synchronized (mDeviceListLock) { 604 closeDeviceInternal(deviceId); 605 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache()); 606 } 607 mRootScanner.resume(); 608 } 609 getOpenedDeviceRecordsCache()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 */ getDeviceDocumentId(int deviceId)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 */ resumeRootScanner()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 shutdown()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 notifyChildDocumentsChange(String parentDocumentId)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 */ resume()676 private void resume() { 677 synchronized (mDeviceListLock) { 678 mDatabase.getMapper().clearMapping(); 679 } 680 } 681 closeDeviceInternal(int deviceId)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 getDeviceToolkit(int deviceId)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 getPipeManager(Identifier identifier)705 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { 706 return getDeviceToolkit(identifier.mDeviceId).mPipeManager; 707 } 708 getDocumentLoader(Identifier identifier)709 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { 710 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; 711 } 712 getFileSize(String documentId)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 */ createErrorCursor(String[] projection, int stringResId)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 DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database, MtpDeviceRecord record)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 close()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 MtpProxyFileDescriptorCallback(int inode)771 MtpProxyFileDescriptorCallback(int inode) { 772 mInode = inode; 773 } 774 775 @Override onGetSize()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 onRead(long offset, int size, byte[] data)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 onWrite(long offset, int size, byte[] data)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 onFsync()823 public void onFsync() throws ErrnoException { 824 tryFsync(); 825 } 826 827 @Override onRelease()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 tryFsync()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