1 /* 2 * Copyright (C) 2013 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.externalstorage; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.usage.StorageStatsManager; 22 import android.content.AttributionSource; 23 import android.content.ContentResolver; 24 import android.content.UriPermission; 25 import android.database.Cursor; 26 import android.database.MatrixCursor; 27 import android.database.MatrixCursor.RowBuilder; 28 import android.net.Uri; 29 import android.os.Binder; 30 import android.os.Bundle; 31 import android.os.Environment; 32 import android.os.UserHandle; 33 import android.os.UserManager; 34 import android.os.storage.DiskInfo; 35 import android.os.storage.StorageEventListener; 36 import android.os.storage.StorageManager; 37 import android.os.storage.VolumeInfo; 38 import android.provider.DocumentsContract; 39 import android.provider.DocumentsContract.Document; 40 import android.provider.DocumentsContract.Path; 41 import android.provider.DocumentsContract.Root; 42 import android.provider.Settings; 43 import android.system.ErrnoException; 44 import android.system.Os; 45 import android.system.OsConstants; 46 import android.text.TextUtils; 47 import android.util.ArrayMap; 48 import android.util.DebugUtils; 49 import android.util.Log; 50 import android.util.Pair; 51 52 import com.android.internal.annotations.GuardedBy; 53 import com.android.internal.annotations.VisibleForTesting; 54 import com.android.internal.content.FileSystemProvider; 55 import com.android.internal.util.IndentingPrintWriter; 56 57 import java.io.File; 58 import java.io.FileDescriptor; 59 import java.io.FileNotFoundException; 60 import java.io.IOException; 61 import java.io.PrintWriter; 62 import java.util.Collections; 63 import java.util.List; 64 import java.util.Objects; 65 import java.util.UUID; 66 67 public class ExternalStorageProvider extends FileSystemProvider { 68 private static final String TAG = "ExternalStorage"; 69 70 private static final boolean DEBUG = false; 71 72 public static final String AUTHORITY = DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY; 73 74 private static final Uri BASE_URI = 75 new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build(); 76 77 // docId format: root:path/to/file 78 79 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 80 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, 81 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_QUERY_ARGS 82 }; 83 84 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 85 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 86 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 87 }; 88 89 private static class RootInfo { 90 public String rootId; 91 public String volumeId; 92 public UUID storageUuid; 93 public int flags; 94 public String title; 95 public String docId; 96 public File visiblePath; 97 public File path; 98 // TODO (b/157033915): Make getFreeBytes() faster 99 public boolean reportAvailableBytes = false; 100 } 101 102 private static final String ROOT_ID_PRIMARY_EMULATED = 103 DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID; 104 105 private static final String GET_DOCUMENT_URI_CALL = "get_document_uri"; 106 private static final String GET_MEDIA_URI_CALL = "get_media_uri"; 107 108 private StorageManager mStorageManager; 109 private UserManager mUserManager; 110 111 private final Object mRootsLock = new Object(); 112 113 @GuardedBy("mRootsLock") 114 private ArrayMap<String, RootInfo> mRoots = new ArrayMap<>(); 115 116 @Override onCreate()117 public boolean onCreate() { 118 super.onCreate(DEFAULT_DOCUMENT_PROJECTION); 119 120 mStorageManager = getContext().getSystemService(StorageManager.class); 121 mUserManager = getContext().getSystemService(UserManager.class); 122 123 updateVolumes(); 124 125 mStorageManager.registerListener(new StorageEventListener() { 126 @Override 127 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 128 updateVolumes(); 129 } 130 }); 131 132 return true; 133 } 134 enforceShellRestrictions()135 private void enforceShellRestrictions() { 136 if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID 137 && mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) { 138 throw new SecurityException( 139 "Shell user cannot access files for user " + UserHandle.myUserId()); 140 } 141 } 142 143 @Override enforceReadPermissionInner(Uri uri, @NonNull AttributionSource attributionSource)144 protected int enforceReadPermissionInner(Uri uri, 145 @NonNull AttributionSource attributionSource) throws SecurityException { 146 enforceShellRestrictions(); 147 return super.enforceReadPermissionInner(uri, attributionSource); 148 } 149 150 @Override enforceWritePermissionInner(Uri uri, @NonNull AttributionSource attributionSource)151 protected int enforceWritePermissionInner(Uri uri, 152 @NonNull AttributionSource attributionSource) throws SecurityException { 153 enforceShellRestrictions(); 154 return super.enforceWritePermissionInner(uri, attributionSource); 155 } 156 updateVolumes()157 public void updateVolumes() { 158 synchronized (mRootsLock) { 159 updateVolumesLocked(); 160 } 161 } 162 163 @GuardedBy("mRootsLock") updateVolumesLocked()164 private void updateVolumesLocked() { 165 mRoots.clear(); 166 167 final int userId = UserHandle.myUserId(); 168 final List<VolumeInfo> volumes = mStorageManager.getVolumes(); 169 for (VolumeInfo volume : volumes) { 170 if (!volume.isMountedReadable() || volume.getMountUserId() != userId) continue; 171 172 final String rootId; 173 final String title; 174 final UUID storageUuid; 175 if (volume.getType() == VolumeInfo.TYPE_EMULATED) { 176 // We currently only support a single emulated volume per user mounted at 177 // a time, and it's always considered the primary 178 if (DEBUG) Log.d(TAG, "Found primary volume: " + volume); 179 rootId = ROOT_ID_PRIMARY_EMULATED; 180 181 if (volume.isPrimaryEmulatedForUser(userId)) { 182 // This is basically the user's primary device storage. 183 // Use device name for the volume since this is likely same thing 184 // the user sees when they mount their phone on another device. 185 String deviceName = Settings.Global.getString( 186 getContext().getContentResolver(), Settings.Global.DEVICE_NAME); 187 188 // Device name should always be set. In case it isn't, though, 189 // fall back to a localized "Internal Storage" string. 190 title = !TextUtils.isEmpty(deviceName) 191 ? deviceName 192 : getContext().getString(R.string.root_internal_storage); 193 storageUuid = StorageManager.UUID_DEFAULT; 194 } else { 195 // This should cover all other storage devices, like an SD card 196 // or USB OTG drive plugged in. Using getBestVolumeDescription() 197 // will give us a nice string like "Samsung SD card" or "SanDisk USB drive" 198 final VolumeInfo privateVol = mStorageManager.findPrivateForEmulated(volume); 199 title = mStorageManager.getBestVolumeDescription(privateVol); 200 storageUuid = StorageManager.convert(privateVol.fsUuid); 201 } 202 } else if (volume.getType() == VolumeInfo.TYPE_PUBLIC 203 || volume.getType() == VolumeInfo.TYPE_STUB) { 204 rootId = volume.getFsUuid(); 205 title = mStorageManager.getBestVolumeDescription(volume); 206 storageUuid = null; 207 } else { 208 // Unsupported volume; ignore 209 continue; 210 } 211 212 if (TextUtils.isEmpty(rootId)) { 213 Log.d(TAG, "Missing UUID for " + volume.getId() + "; skipping"); 214 continue; 215 } 216 if (mRoots.containsKey(rootId)) { 217 Log.w(TAG, "Duplicate UUID " + rootId + " for " + volume.getId() + "; skipping"); 218 continue; 219 } 220 221 final RootInfo root = new RootInfo(); 222 mRoots.put(rootId, root); 223 224 root.rootId = rootId; 225 root.volumeId = volume.id; 226 root.storageUuid = storageUuid; 227 root.flags = Root.FLAG_LOCAL_ONLY 228 | Root.FLAG_SUPPORTS_SEARCH 229 | Root.FLAG_SUPPORTS_IS_CHILD; 230 231 final DiskInfo disk = volume.getDisk(); 232 if (DEBUG) Log.d(TAG, "Disk for root " + rootId + " is " + disk); 233 if (disk != null && disk.isSd()) { 234 root.flags |= Root.FLAG_REMOVABLE_SD; 235 } else if (disk != null && disk.isUsb()) { 236 root.flags |= Root.FLAG_REMOVABLE_USB; 237 } 238 239 if (volume.getType() != VolumeInfo.TYPE_EMULATED) { 240 root.flags |= Root.FLAG_SUPPORTS_EJECT; 241 } 242 243 if (volume.isPrimary()) { 244 root.flags |= Root.FLAG_ADVANCED; 245 } 246 // Dunno when this would NOT be the case, but never hurts to be correct. 247 if (volume.isMountedWritable()) { 248 root.flags |= Root.FLAG_SUPPORTS_CREATE; 249 } 250 root.title = title; 251 if (volume.getType() == VolumeInfo.TYPE_PUBLIC) { 252 root.flags |= Root.FLAG_HAS_SETTINGS; 253 } 254 if (volume.isVisibleForRead(userId)) { 255 root.visiblePath = volume.getPathForUser(userId); 256 } else { 257 root.visiblePath = null; 258 } 259 root.path = volume.getInternalPathForUser(userId); 260 try { 261 root.docId = getDocIdForFile(root.path); 262 } catch (FileNotFoundException e) { 263 throw new IllegalStateException(e); 264 } 265 } 266 267 Log.d(TAG, "After updating volumes, found " + mRoots.size() + " active roots"); 268 269 // Note this affects content://com.android.externalstorage.documents/root/39BD-07C5 270 // as well as content://com.android.externalstorage.documents/document/*/children, 271 // so just notify on content://com.android.externalstorage.documents/. 272 getContext().getContentResolver().notifyChange(BASE_URI, null, false); 273 } 274 resolveRootProjection(String[] projection)275 private static String[] resolveRootProjection(String[] projection) { 276 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 277 } 278 279 @Override queryChildDocumentsForManage( String parentDocId, String[] projection, String sortOrder)280 public Cursor queryChildDocumentsForManage( 281 String parentDocId, String[] projection, String sortOrder) 282 throws FileNotFoundException { 283 return queryChildDocumentsShowAll(parentDocId, projection, sortOrder); 284 } 285 286 /** 287 * Check that the directory is the root of storage or blocked file from tree. 288 * 289 * @param docId the docId of the directory to be checked 290 * @return true, should be blocked from tree. Otherwise, false. 291 */ 292 @Override shouldBlockFromTree(@onNull String docId)293 protected boolean shouldBlockFromTree(@NonNull String docId) { 294 try { 295 final File dir = getFileForDocId(docId, false /* visible */); 296 297 // the file is null or it is not a directory 298 if (dir == null || !dir.isDirectory()) { 299 return false; 300 } 301 302 // Allow all directories on USB, including the root. 303 try { 304 RootInfo rootInfo = getRootFromDocId(docId); 305 if ((rootInfo.flags & Root.FLAG_REMOVABLE_USB) == Root.FLAG_REMOVABLE_USB) { 306 return false; 307 } 308 } catch (FileNotFoundException e) { 309 Log.e(TAG, "Failed to determine rootInfo for docId"); 310 } 311 312 final String path = getPathFromDocId(docId); 313 314 // Block the root of the storage 315 if (path.isEmpty()) { 316 return true; 317 } 318 319 // Block Download folder from tree 320 if (TextUtils.equals(Environment.DIRECTORY_DOWNLOADS.toLowerCase(), 321 path.toLowerCase())) { 322 return true; 323 } 324 325 if (TextUtils.equals(Environment.DIRECTORY_ANDROID.toLowerCase(), 326 path.toLowerCase())) { 327 return true; 328 } 329 330 return false; 331 } catch (IOException e) { 332 throw new IllegalArgumentException( 333 "Failed to determine if " + docId + " should block from tree " + ": " + e); 334 } 335 } 336 337 @Override getDocIdForFile(File file)338 protected String getDocIdForFile(File file) throws FileNotFoundException { 339 return getDocIdForFileMaybeCreate(file, false); 340 } 341 getDocIdForFileMaybeCreate(File file, boolean createNewDir)342 private String getDocIdForFileMaybeCreate(File file, boolean createNewDir) 343 throws FileNotFoundException { 344 String path = file.getAbsolutePath(); 345 346 // Find the most-specific root path 347 boolean visiblePath = false; 348 RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false); 349 350 if (mostSpecificRoot == null) { 351 // Try visible path if no internal path matches. MediaStore uses visible paths. 352 visiblePath = true; 353 mostSpecificRoot = getMostSpecificRootForPath(path, true); 354 } 355 356 if (mostSpecificRoot == null) { 357 throw new FileNotFoundException("Failed to find root that contains " + path); 358 } 359 360 // Start at first char of path under root 361 final String rootPath = visiblePath 362 ? mostSpecificRoot.visiblePath.getAbsolutePath() 363 : mostSpecificRoot.path.getAbsolutePath(); 364 if (rootPath.equals(path)) { 365 path = ""; 366 } else if (rootPath.endsWith("/")) { 367 path = path.substring(rootPath.length()); 368 } else { 369 path = path.substring(rootPath.length() + 1); 370 } 371 372 if (!file.exists() && createNewDir) { 373 Log.i(TAG, "Creating new directory " + file); 374 if (!file.mkdir()) { 375 Log.e(TAG, "Could not create directory " + file); 376 } 377 } 378 379 return mostSpecificRoot.rootId + ':' + path; 380 } 381 getMostSpecificRootForPath(String path, boolean visible)382 private RootInfo getMostSpecificRootForPath(String path, boolean visible) { 383 // Find the most-specific root path 384 RootInfo mostSpecificRoot = null; 385 String mostSpecificPath = null; 386 synchronized (mRootsLock) { 387 for (int i = 0; i < mRoots.size(); i++) { 388 final RootInfo root = mRoots.valueAt(i); 389 final File rootFile = visible ? root.visiblePath : root.path; 390 if (rootFile != null) { 391 final String rootPath = rootFile.getAbsolutePath(); 392 if (path.startsWith(rootPath) && (mostSpecificPath == null 393 || rootPath.length() > mostSpecificPath.length())) { 394 mostSpecificRoot = root; 395 mostSpecificPath = rootPath; 396 } 397 } 398 } 399 } 400 401 return mostSpecificRoot; 402 } 403 404 @Override getFileForDocId(String docId, boolean visible)405 protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException { 406 return getFileForDocId(docId, visible, true); 407 } 408 getFileForDocId(String docId, boolean visible, boolean mustExist)409 private File getFileForDocId(String docId, boolean visible, boolean mustExist) 410 throws FileNotFoundException { 411 RootInfo root = getRootFromDocId(docId); 412 return buildFile(root, docId, visible, mustExist); 413 } 414 resolveDocId(String docId, boolean visible)415 private Pair<RootInfo, File> resolveDocId(String docId, boolean visible) 416 throws FileNotFoundException { 417 RootInfo root = getRootFromDocId(docId); 418 return Pair.create(root, buildFile(root, docId, visible, true)); 419 } 420 421 @VisibleForTesting getPathFromDocId(String docId)422 static String getPathFromDocId(String docId) { 423 final int splitIndex = docId.indexOf(':', 1); 424 final String path = docId.substring(splitIndex + 1); 425 426 if (path.isEmpty()) { 427 return path; 428 } 429 430 // remove trailing "/" 431 if (path.charAt(path.length() - 1) == '/') { 432 return path.substring(0, path.length() - 1); 433 } else { 434 return path; 435 } 436 } 437 getRootFromDocId(String docId)438 private RootInfo getRootFromDocId(String docId) throws FileNotFoundException { 439 final int splitIndex = docId.indexOf(':', 1); 440 final String tag = docId.substring(0, splitIndex); 441 442 RootInfo root; 443 synchronized (mRootsLock) { 444 root = mRoots.get(tag); 445 } 446 if (root == null) { 447 throw new FileNotFoundException("No root for " + tag); 448 } 449 450 return root; 451 } 452 buildFile(RootInfo root, String docId, boolean visible, boolean mustExist)453 private File buildFile(RootInfo root, String docId, boolean visible, boolean mustExist) 454 throws FileNotFoundException { 455 final int splitIndex = docId.indexOf(':', 1); 456 final String path = docId.substring(splitIndex + 1); 457 458 File target = root.visiblePath != null ? root.visiblePath : root.path; 459 if (target == null) { 460 return null; 461 } 462 if (!target.exists()) { 463 target.mkdirs(); 464 } 465 target = new File(target, path); 466 if (mustExist && !target.exists()) { 467 throw new FileNotFoundException("Missing file for " + docId + " at " + target); 468 } 469 return target; 470 } 471 472 @Override buildNotificationUri(String docId)473 protected Uri buildNotificationUri(String docId) { 474 return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId); 475 } 476 477 @Override onDocIdChanged(String docId)478 protected void onDocIdChanged(String docId) { 479 try { 480 // Touch the visible path to ensure that any sdcardfs caches have 481 // been updated to reflect underlying changes on disk. 482 final File visiblePath = getFileForDocId(docId, true, false); 483 if (visiblePath != null) { 484 Os.access(visiblePath.getAbsolutePath(), OsConstants.F_OK); 485 } 486 } catch (FileNotFoundException | ErrnoException ignored) { 487 } 488 } 489 490 @Override onDocIdDeleted(String docId)491 protected void onDocIdDeleted(String docId) { 492 Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, docId); 493 getContext().revokeUriPermission(uri, ~0); 494 } 495 496 497 @Override queryRoots(String[] projection)498 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 499 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 500 synchronized (mRootsLock) { 501 for (RootInfo root : mRoots.values()) { 502 final RowBuilder row = result.newRow(); 503 row.add(Root.COLUMN_ROOT_ID, root.rootId); 504 row.add(Root.COLUMN_FLAGS, root.flags); 505 row.add(Root.COLUMN_TITLE, root.title); 506 row.add(Root.COLUMN_DOCUMENT_ID, root.docId); 507 row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS); 508 509 long availableBytes = -1; 510 if (root.reportAvailableBytes) { 511 if (root.storageUuid != null) { 512 try { 513 availableBytes = getContext() 514 .getSystemService(StorageStatsManager.class) 515 .getFreeBytes(root.storageUuid); 516 } catch (IOException e) { 517 Log.w(TAG, e); 518 } 519 } else { 520 availableBytes = root.path.getUsableSpace(); 521 } 522 } 523 row.add(Root.COLUMN_AVAILABLE_BYTES, availableBytes); 524 } 525 } 526 return result; 527 } 528 529 @Override findDocumentPath(@ullable String parentDocId, String childDocId)530 public Path findDocumentPath(@Nullable String parentDocId, String childDocId) 531 throws FileNotFoundException { 532 final Pair<RootInfo, File> resolvedDocId = resolveDocId(childDocId, false); 533 final RootInfo root = resolvedDocId.first; 534 File child = resolvedDocId.second; 535 536 final File rootFile = root.visiblePath != null ? root.visiblePath 537 : root.path; 538 final File parent = TextUtils.isEmpty(parentDocId) 539 ? rootFile 540 : getFileForDocId(parentDocId); 541 542 return new Path(parentDocId == null ? root.rootId : null, findDocumentPath(parent, child)); 543 } 544 getDocumentUri(String path, List<UriPermission> accessUriPermissions)545 private Uri getDocumentUri(String path, List<UriPermission> accessUriPermissions) 546 throws FileNotFoundException { 547 File doc = new File(path); 548 549 final String docId = getDocIdForFile(doc); 550 551 UriPermission docUriPermission = null; 552 UriPermission treeUriPermission = null; 553 for (UriPermission uriPermission : accessUriPermissions) { 554 final Uri uri = uriPermission.getUri(); 555 if (AUTHORITY.equals(uri.getAuthority())) { 556 boolean matchesRequestedDoc = false; 557 if (DocumentsContract.isTreeUri(uri)) { 558 final String parentDocId = DocumentsContract.getTreeDocumentId(uri); 559 if (isChildDocument(parentDocId, docId)) { 560 treeUriPermission = uriPermission; 561 matchesRequestedDoc = true; 562 } 563 } else { 564 final String candidateDocId = DocumentsContract.getDocumentId(uri); 565 if (Objects.equals(docId, candidateDocId)) { 566 docUriPermission = uriPermission; 567 matchesRequestedDoc = true; 568 } 569 } 570 571 if (matchesRequestedDoc && allowsBothReadAndWrite(uriPermission)) { 572 // This URI permission provides everything an app can get, no need to 573 // further check any other granted URI. 574 break; 575 } 576 } 577 } 578 579 // Full permission URI first. 580 if (allowsBothReadAndWrite(treeUriPermission)) { 581 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId); 582 } 583 584 if (allowsBothReadAndWrite(docUriPermission)) { 585 return docUriPermission.getUri(); 586 } 587 588 // Then partial permission URI. 589 if (treeUriPermission != null) { 590 return DocumentsContract.buildDocumentUriUsingTree(treeUriPermission.getUri(), docId); 591 } 592 593 if (docUriPermission != null) { 594 return docUriPermission.getUri(); 595 } 596 597 throw new SecurityException("The app is not given any access to the document under path " + 598 path + " with permissions granted in " + accessUriPermissions); 599 } 600 allowsBothReadAndWrite(UriPermission permission)601 private static boolean allowsBothReadAndWrite(UriPermission permission) { 602 return permission != null 603 && permission.isReadPermission() 604 && permission.isWritePermission(); 605 } 606 607 @Override querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)608 public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs) 609 throws FileNotFoundException { 610 final File parent; 611 612 synchronized (mRootsLock) { 613 RootInfo root = mRoots.get(rootId); 614 parent = root.visiblePath != null ? root.visiblePath 615 : root.path; 616 } 617 618 return querySearchDocuments(parent, projection, Collections.emptySet(), queryArgs); 619 } 620 621 @Override ejectRoot(String rootId)622 public void ejectRoot(String rootId) { 623 final long token = Binder.clearCallingIdentity(); 624 RootInfo root = mRoots.get(rootId); 625 if (root != null) { 626 try { 627 mStorageManager.unmount(root.volumeId); 628 } catch (RuntimeException e) { 629 throw new IllegalStateException(e); 630 } finally { 631 Binder.restoreCallingIdentity(token); 632 } 633 } 634 } 635 636 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)637 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 638 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160); 639 synchronized (mRootsLock) { 640 for (int i = 0; i < mRoots.size(); i++) { 641 final RootInfo root = mRoots.valueAt(i); 642 pw.println("Root{" + root.rootId + "}:"); 643 pw.increaseIndent(); 644 pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags)); 645 pw.println(); 646 pw.printPair("title", root.title); 647 pw.printPair("docId", root.docId); 648 pw.println(); 649 pw.printPair("path", root.path); 650 pw.printPair("visiblePath", root.visiblePath); 651 pw.decreaseIndent(); 652 pw.println(); 653 } 654 } 655 } 656 657 @Override call(String method, String arg, Bundle extras)658 public Bundle call(String method, String arg, Bundle extras) { 659 Bundle bundle = super.call(method, arg, extras); 660 if (bundle == null && !TextUtils.isEmpty(method)) { 661 switch (method) { 662 case "getDocIdForFileCreateNewDir": { 663 getContext().enforceCallingPermission( 664 android.Manifest.permission.MANAGE_DOCUMENTS, null); 665 if (TextUtils.isEmpty(arg)) { 666 return null; 667 } 668 try { 669 final String docId = getDocIdForFileMaybeCreate(new File(arg), true); 670 bundle = new Bundle(); 671 bundle.putString("DOC_ID", docId); 672 } catch (FileNotFoundException e) { 673 Log.w(TAG, "file '" + arg + "' not found"); 674 return null; 675 } 676 break; 677 } 678 case GET_DOCUMENT_URI_CALL: { 679 // All callers must go through MediaProvider 680 getContext().enforceCallingPermission( 681 android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG); 682 683 final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 684 final List<UriPermission> accessUriPermissions = extras 685 .getParcelableArrayList(DocumentsContract.EXTRA_URI_PERMISSIONS); 686 687 final String path = fileUri.getPath(); 688 try { 689 final Bundle out = new Bundle(); 690 final Uri uri = getDocumentUri(path, accessUriPermissions); 691 out.putParcelable(DocumentsContract.EXTRA_URI, uri); 692 return out; 693 } catch (FileNotFoundException e) { 694 throw new IllegalStateException("File in " + path + " is not found.", e); 695 } 696 } 697 case GET_MEDIA_URI_CALL: { 698 // All callers must go through MediaProvider 699 getContext().enforceCallingPermission( 700 android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG); 701 702 final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 703 final String docId = DocumentsContract.getDocumentId(documentUri); 704 try { 705 final Bundle out = new Bundle(); 706 final Uri uri = Uri.fromFile(getFileForDocId(docId, true)); 707 out.putParcelable(DocumentsContract.EXTRA_URI, uri); 708 return out; 709 } catch (FileNotFoundException e) { 710 throw new IllegalStateException(e); 711 } 712 } 713 default: 714 Log.w(TAG, "unknown method passed to call(): " + method); 715 } 716 } 717 return bundle; 718 } 719 } 720