1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.mtp; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.UriPermission; 23 import android.content.res.AssetFileDescriptor; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.database.DatabaseUtils; 27 import android.database.MatrixCursor; 28 import android.database.sqlite.SQLiteDiskIOException; 29 import android.graphics.Point; 30 import android.media.MediaFile; 31 import android.mtp.MtpConstants; 32 import android.mtp.MtpObjectInfo; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.CancellationSignal; 36 import android.os.FileUtils; 37 import android.os.ParcelFileDescriptor; 38 import android.os.ProxyFileDescriptorCallback; 39 import android.os.storage.StorageManager; 40 import android.provider.DocumentsContract.Document; 41 import android.provider.DocumentsContract.Path; 42 import android.provider.DocumentsContract.Root; 43 import android.provider.DocumentsContract; 44 import android.provider.DocumentsProvider; 45 import android.provider.Settings; 46 import android.system.ErrnoException; 47 import android.system.OsConstants; 48 import android.util.Log; 49 50 import com.android.internal.annotations.GuardedBy; 51 import com.android.internal.annotations.VisibleForTesting; 52 53 import java.io.FileNotFoundException; 54 import java.io.IOException; 55 import java.util.HashMap; 56 import java.util.LinkedList; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.concurrent.TimeoutException; 60 import libcore.io.IoUtils; 61 62 /** 63 * DocumentsProvider for MTP devices. 64 */ 65 public 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 */ getInstance()99 static MtpDocumentsProvider getInstance() { 100 return sSingleton; 101 } 102 103 @Override onCreate()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 onCreateForTesting( Context context, Resources resources, MtpManager mtpManager, ContentResolver resolver, MtpDatabase database, StorageManager storageManager, ServiceIntentSender intentSender)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 queryRoots(String[] projection)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 queryDocument(String documentId, String[] projection)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 queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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 openDocument( String documentId, String mode, CancellationSignal signal)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 openDocumentThumbnail( String documentId, Point sizeHint, CancellationSignal signal)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 deleteDocument(String documentId)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 onTrimMemory(int level)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 createDocument(String parentDocumentId, String mimeType, String displayName)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 findDocumentPath(String parentDocumentId, String childDocumentId)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 isChildDocument(String parentDocumentId, String documentId)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 openDevice(int deviceId)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 closeDevice(int deviceId)575 void closeDevice(int deviceId) throws IOException, InterruptedException { 576 synchronized (mDeviceListLock) { 577 closeDeviceInternal(deviceId); 578 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache()); 579 } 580 mRootScanner.resume(); 581 } 582 getOpenedDeviceRecordsCache()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 */ getDeviceDocumentId(int deviceId)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 */ resumeRootScanner()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 shutdown()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 notifyChildDocumentsChange(String parentDocumentId)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 */ resume()649 private void resume() { 650 synchronized (mDeviceListLock) { 651 mDatabase.getMapper().clearMapping(); 652 } 653 } 654 closeDeviceInternal(int deviceId)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 getDeviceToolkit(int deviceId)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 getPipeManager(Identifier identifier)678 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { 679 return getDeviceToolkit(identifier.mDeviceId).mPipeManager; 680 } 681 getDocumentLoader(Identifier identifier)682 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { 683 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; 684 } 685 getFileSize(String documentId)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 */ createErrorCursor(String[] projection, int stringResId)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 DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database, MtpDeviceRecord record)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 close()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 MtpProxyFileDescriptorCallback(int inode)744 MtpProxyFileDescriptorCallback(int inode) { 745 mInode = inode; 746 } 747 748 @Override onGetSize()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 onRead(long offset, int size, byte[] data)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 onWrite(long offset, int size, byte[] data)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 onFsync()796 public void onFsync() throws ErrnoException { 797 tryFsync(); 798 } 799 800 @Override onRelease()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 tryFsync()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