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