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