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