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