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