1 /* 2 * Copyright (C) 2011 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 android.os.storage; 18 19 import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SuppressLint; 24 import android.annotation.SystemApi; 25 import android.annotation.TestApi; 26 import android.compat.annotation.UnsupportedAppUsage; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.net.Uri; 30 import android.os.Build; 31 import android.os.Environment; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 import android.os.UserHandle; 35 import android.provider.DocumentsContract; 36 import android.provider.MediaStore; 37 38 import com.android.internal.util.IndentingPrintWriter; 39 import com.android.internal.util.Preconditions; 40 41 import java.io.CharArrayWriter; 42 import java.io.File; 43 import java.util.Locale; 44 import java.util.UUID; 45 46 /** 47 * Information about a shared/external storage volume for a specific user. 48 * 49 * <p> 50 * A device always has one (and one only) primary storage volume, but it could have extra volumes, 51 * like SD cards and USB drives. This object represents the logical view of a storage 52 * volume for a specific user: different users might have different views for the same physical 53 * volume (for example, if the volume is a built-in emulated storage). 54 * 55 * <p> 56 * The storage volume is not necessarily mounted, applications should use {@link #getState()} to 57 * verify its state. 58 * 59 * <p> 60 * Applications willing to read or write to this storage volume needs to get a permission from the 61 * user first, which can be achieved in the following ways: 62 * 63 * <ul> 64 * <li>To get access to standard directories (like the {@link Environment#DIRECTORY_PICTURES}), they 65 * can use the {@link #createAccessIntent(String)}. This is the recommend way, since it provides a 66 * simpler API and narrows the access to the given directory (and its descendants). 67 * <li>To get access to any directory (and its descendants), they can use the Storage Acess 68 * Framework APIs (such as {@link Intent#ACTION_OPEN_DOCUMENT} and 69 * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, although these APIs do not guarantee the user will 70 * select this specific volume. 71 * <li>To get read and write access to the primary storage volume, applications can declare the 72 * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and 73 * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions respectively, with the 74 * latter including the former. This approach is discouraged, since users may be hesitant to grant 75 * broad access to all files contained on a storage device. 76 * </ul> 77 * 78 * <p>It can be obtained through {@link StorageManager#getStorageVolumes()} and 79 * {@link StorageManager#getPrimaryStorageVolume()} and also as an extra in some broadcasts 80 * (see {@link #EXTRA_STORAGE_VOLUME}). 81 * 82 * <p> 83 * See {@link Environment#getExternalStorageDirectory()} for more info about shared/external 84 * storage semantics. 85 */ 86 // NOTE: This is a legacy specialization of VolumeInfo which describes the volume for a specific 87 // user, but is now part of the public API. 88 public final class StorageVolume implements Parcelable { 89 90 @UnsupportedAppUsage 91 private final String mId; 92 @UnsupportedAppUsage 93 private final File mPath; 94 private final File mInternalPath; 95 @UnsupportedAppUsage 96 private final String mDescription; 97 @UnsupportedAppUsage 98 private final boolean mPrimary; 99 @UnsupportedAppUsage 100 private final boolean mRemovable; 101 private final boolean mEmulated; 102 private final boolean mAllowMassStorage; 103 private final long mMaxFileSize; 104 private final UserHandle mOwner; 105 private final UUID mUuid; 106 private final String mFsUuid; 107 private final String mState; 108 109 /** 110 * Name of the {@link Parcelable} extra in the {@link Intent#ACTION_MEDIA_REMOVED}, 111 * {@link Intent#ACTION_MEDIA_UNMOUNTED}, {@link Intent#ACTION_MEDIA_CHECKING}, 112 * {@link Intent#ACTION_MEDIA_NOFS}, {@link Intent#ACTION_MEDIA_MOUNTED}, 113 * {@link Intent#ACTION_MEDIA_SHARED}, {@link Intent#ACTION_MEDIA_BAD_REMOVAL}, 114 * {@link Intent#ACTION_MEDIA_UNMOUNTABLE}, and {@link Intent#ACTION_MEDIA_EJECT} broadcast that 115 * contains a {@link StorageVolume}. 116 */ 117 // Also sent on ACTION_MEDIA_UNSHARED, which is @hide 118 public static final String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME"; 119 120 /** 121 * Name of the String extra used by {@link #createAccessIntent(String) createAccessIntent}. 122 * 123 * @hide 124 */ 125 public static final String EXTRA_DIRECTORY_NAME = "android.os.storage.extra.DIRECTORY_NAME"; 126 127 /** 128 * Name of the intent used by {@link #createAccessIntent(String) createAccessIntent}. 129 */ 130 private static final String ACTION_OPEN_EXTERNAL_DIRECTORY = 131 "android.os.storage.action.OPEN_EXTERNAL_DIRECTORY"; 132 133 /** {@hide} */ 134 public static final int STORAGE_ID_INVALID = 0x00000000; 135 /** {@hide} */ 136 public static final int STORAGE_ID_PRIMARY = 0x00010001; 137 138 /** {@hide} */ StorageVolume(String id, File path, File internalPath, String description, boolean primary, boolean removable, boolean emulated, boolean allowMassStorage, long maxFileSize, UserHandle owner, UUID uuid, String fsUuid, String state)139 public StorageVolume(String id, File path, File internalPath, String description, 140 boolean primary, boolean removable, boolean emulated, boolean allowMassStorage, 141 long maxFileSize, UserHandle owner, UUID uuid, String fsUuid, String state) { 142 mId = Preconditions.checkNotNull(id); 143 mPath = Preconditions.checkNotNull(path); 144 mInternalPath = Preconditions.checkNotNull(internalPath); 145 mDescription = Preconditions.checkNotNull(description); 146 mPrimary = primary; 147 mRemovable = removable; 148 mEmulated = emulated; 149 mAllowMassStorage = allowMassStorage; 150 mMaxFileSize = maxFileSize; 151 mOwner = Preconditions.checkNotNull(owner); 152 mUuid = uuid; 153 mFsUuid = fsUuid; 154 mState = Preconditions.checkNotNull(state); 155 } 156 StorageVolume(Parcel in)157 private StorageVolume(Parcel in) { 158 mId = in.readString8(); 159 mPath = new File(in.readString8()); 160 mInternalPath = new File(in.readString8()); 161 mDescription = in.readString8(); 162 mPrimary = in.readInt() != 0; 163 mRemovable = in.readInt() != 0; 164 mEmulated = in.readInt() != 0; 165 mAllowMassStorage = in.readInt() != 0; 166 mMaxFileSize = in.readLong(); 167 mOwner = in.readParcelable(null); 168 if (in.readInt() != 0) { 169 mUuid = StorageManager.convert(in.readString8()); 170 } else { 171 mUuid = null; 172 } 173 mFsUuid = in.readString8(); 174 mState = in.readString8(); 175 } 176 177 /** 178 * Return an opaque ID that can be used to identify this volume. 179 * 180 * @hide 181 */ 182 @SystemApi getId()183 public @NonNull String getId() { 184 return mId; 185 } 186 187 /** 188 * Returns the mount path for the volume. 189 * 190 * @return the mount path 191 * @hide 192 */ 193 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}") 194 @TestApi getPath()195 public String getPath() { 196 return mPath.toString(); 197 } 198 199 /** 200 * Returns the path of the underlying filesystem. 201 * 202 * @return the internal path 203 * @hide 204 */ getInternalPath()205 public String getInternalPath() { 206 return mInternalPath.toString(); 207 } 208 209 /** {@hide} */ 210 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}") getPathFile()211 public File getPathFile() { 212 return mPath; 213 } 214 215 /** 216 * Returns the directory where this volume is currently mounted. 217 * <p> 218 * Direct filesystem access via this path has significant emulation 219 * overhead, and apps are instead strongly encouraged to interact with media 220 * on storage volumes via the {@link MediaStore} APIs. 221 * <p> 222 * This directory does not give apps any additional access beyond what they 223 * already have via {@link MediaStore}. 224 * 225 * @return directory where this volume is mounted, or {@code null} if the 226 * volume is not currently mounted. 227 */ getDirectory()228 public @Nullable File getDirectory() { 229 switch (mState) { 230 case Environment.MEDIA_MOUNTED: 231 case Environment.MEDIA_MOUNTED_READ_ONLY: 232 return mPath; 233 default: 234 return null; 235 } 236 } 237 238 /** 239 * Returns a user-visible description of the volume. 240 * 241 * @return the volume description 242 */ getDescription(Context context)243 public String getDescription(Context context) { 244 return mDescription; 245 } 246 247 /** 248 * Returns true if the volume is the primary shared/external storage, which is the volume 249 * backed by {@link Environment#getExternalStorageDirectory()}. 250 */ isPrimary()251 public boolean isPrimary() { 252 return mPrimary; 253 } 254 255 /** 256 * Returns true if the volume is removable. 257 * 258 * @return is removable 259 */ isRemovable()260 public boolean isRemovable() { 261 return mRemovable; 262 } 263 264 /** 265 * Returns true if the volume is emulated. 266 * 267 * @return is removable 268 */ isEmulated()269 public boolean isEmulated() { 270 return mEmulated; 271 } 272 273 /** 274 * Returns true if this volume can be shared via USB mass storage. 275 * 276 * @return whether mass storage is allowed 277 * @hide 278 */ 279 @UnsupportedAppUsage allowMassStorage()280 public boolean allowMassStorage() { 281 return mAllowMassStorage; 282 } 283 284 /** 285 * Returns maximum file size for the volume, or zero if it is unbounded. 286 * 287 * @return maximum file size 288 * @hide 289 */ 290 @UnsupportedAppUsage getMaxFileSize()291 public long getMaxFileSize() { 292 return mMaxFileSize; 293 } 294 295 /** 296 * Returns the user that owns this volume 297 * 298 * {@hide} 299 */ 300 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 301 @SystemApi(client = MODULE_LIBRARIES) getOwner()302 public @NonNull UserHandle getOwner() { 303 return mOwner; 304 } 305 306 /** 307 * Gets the converted volume UUID. If a valid UUID is returned, it is compatible with other 308 * APIs that make use of {@link UUID} like {@link StorageManager#allocateBytes} and 309 * {@link android.content.pm.ApplicationInfo#storageUuid} 310 * 311 * @return the UUID for the volume or {@code null} for "portable" storage devices which haven't 312 * been adopted. 313 * 314 * @see <a href="https://source.android.com/devices/storage/adoptable">Adoptable storage</a> 315 */ getStorageUuid()316 public @Nullable UUID getStorageUuid() { 317 return mUuid; 318 } 319 320 /** 321 * Gets the volume UUID, if any. 322 */ getUuid()323 public @Nullable String getUuid() { 324 return mFsUuid; 325 } 326 327 /** 328 * Return the volume name that can be used to interact with this storage 329 * device through {@link MediaStore}. 330 * 331 * @return opaque volume name, or {@code null} if this volume is not indexed 332 * by {@link MediaStore}. 333 * @see android.provider.MediaStore.Audio.Media#getContentUri(String) 334 * @see android.provider.MediaStore.Video.Media#getContentUri(String) 335 * @see android.provider.MediaStore.Images.Media#getContentUri(String) 336 */ getMediaStoreVolumeName()337 public @Nullable String getMediaStoreVolumeName() { 338 if (isPrimary()) { 339 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 340 } else { 341 return getNormalizedUuid(); 342 } 343 } 344 345 /** {@hide} */ normalizeUuid(@ullable String fsUuid)346 public static @Nullable String normalizeUuid(@Nullable String fsUuid) { 347 return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null; 348 } 349 350 /** {@hide} */ getNormalizedUuid()351 public @Nullable String getNormalizedUuid() { 352 return normalizeUuid(mFsUuid); 353 } 354 355 /** 356 * Parse and return volume UUID as FAT volume ID, or return -1 if unable to 357 * parse or UUID is unknown. 358 * @hide 359 */ 360 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) getFatVolumeId()361 public int getFatVolumeId() { 362 if (mFsUuid == null || mFsUuid.length() != 9) { 363 return -1; 364 } 365 try { 366 return (int) Long.parseLong(mFsUuid.replace("-", ""), 16); 367 } catch (NumberFormatException e) { 368 return -1; 369 } 370 } 371 372 /** {@hide} */ 373 @UnsupportedAppUsage getUserLabel()374 public String getUserLabel() { 375 return mDescription; 376 } 377 378 /** 379 * Returns the current state of the volume. 380 * 381 * @return one of {@link Environment#MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED}, 382 * {@link Environment#MEDIA_UNMOUNTED}, {@link Environment#MEDIA_CHECKING}, 383 * {@link Environment#MEDIA_NOFS}, {@link Environment#MEDIA_MOUNTED}, 384 * {@link Environment#MEDIA_MOUNTED_READ_ONLY}, {@link Environment#MEDIA_SHARED}, 385 * {@link Environment#MEDIA_BAD_REMOVAL}, or {@link Environment#MEDIA_UNMOUNTABLE}. 386 */ getState()387 public String getState() { 388 return mState; 389 } 390 391 /** 392 * Builds an intent to give access to a standard storage directory or entire volume after 393 * obtaining the user's approval. 394 * <p> 395 * When invoked, the system will ask the user to grant access to the requested directory (and 396 * its descendants). The result of the request will be returned to the activity through the 397 * {@code onActivityResult} method. 398 * <p> 399 * To gain access to descendants (child, grandchild, etc) documents, use 400 * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)}, or 401 * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI. 402 * <p> 403 * If your application only needs to store internal data, consider using 404 * {@link Context#getExternalFilesDirs(String) Context.getExternalFilesDirs}, 405 * {@link Context#getExternalCacheDirs()}, or {@link Context#getExternalMediaDirs()}, which 406 * require no permissions to read or write. 407 * <p> 408 * Access to the entire volume is only available for non-primary volumes (for the primary 409 * volume, apps can use the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and 410 * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions) and should be used 411 * with caution, since users are more likely to deny access when asked for entire volume access 412 * rather than specific directories. 413 * 414 * @param directoryName must be one of {@link Environment#DIRECTORY_MUSIC}, 415 * {@link Environment#DIRECTORY_PODCASTS}, {@link Environment#DIRECTORY_RINGTONES}, 416 * {@link Environment#DIRECTORY_ALARMS}, {@link Environment#DIRECTORY_NOTIFICATIONS}, 417 * {@link Environment#DIRECTORY_PICTURES}, {@link Environment#DIRECTORY_MOVIES}, 418 * {@link Environment#DIRECTORY_DOWNLOADS}, {@link Environment#DIRECTORY_DCIM}, or 419 * {@link Environment#DIRECTORY_DOCUMENTS}, or {@code null} to request access to the 420 * entire volume. 421 * @return intent to request access, or {@code null} if the requested directory is invalid for 422 * that volume. 423 * @see DocumentsContract 424 * @deprecated Callers should migrate to using {@link Intent#ACTION_OPEN_DOCUMENT_TREE} instead. 425 * Launching this {@link Intent} on devices running 426 * {@link android.os.Build.VERSION_CODES#Q} or higher, will immediately finish 427 * with a result code of {@link android.app.Activity#RESULT_CANCELED}. 428 */ 429 @Deprecated createAccessIntent(String directoryName)430 public @Nullable Intent createAccessIntent(String directoryName) { 431 if ((isPrimary() && directoryName == null) || 432 (directoryName != null && !Environment.isStandardDirectory(directoryName))) { 433 return null; 434 } 435 final Intent intent = new Intent(ACTION_OPEN_EXTERNAL_DIRECTORY); 436 intent.putExtra(EXTRA_STORAGE_VOLUME, this); 437 intent.putExtra(EXTRA_DIRECTORY_NAME, directoryName); 438 return intent; 439 } 440 441 /** 442 * Builds an {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to allow the user to grant access to any 443 * directory subtree (or entire volume) from the {@link android.provider.DocumentsProvider}s 444 * available on the device. The initial location of the document navigation will be the root of 445 * this {@link StorageVolume}. 446 * 447 * Note that the returned {@link Intent} simply suggests that the user picks this {@link 448 * StorageVolume} by default, but the user may select a different location. Callers must respect 449 * the user's chosen location, even if it is different from the originally requested location. 450 * 451 * @return intent to {@link Intent#ACTION_OPEN_DOCUMENT_TREE} initially showing the contents 452 * of this {@link StorageVolume} 453 * @see Intent#ACTION_OPEN_DOCUMENT_TREE 454 */ createOpenDocumentTreeIntent()455 @NonNull public Intent createOpenDocumentTreeIntent() { 456 final String rootId = isEmulated() 457 ? DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID 458 : mFsUuid; 459 final Uri rootUri = DocumentsContract.buildRootUri( 460 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, rootId); 461 final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 462 .putExtra(DocumentsContract.EXTRA_INITIAL_URI, rootUri) 463 .putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, true); 464 return intent; 465 } 466 467 @Override equals(@ullable Object obj)468 public boolean equals(@Nullable Object obj) { 469 if (obj instanceof StorageVolume && mPath != null) { 470 StorageVolume volume = (StorageVolume)obj; 471 return (mPath.equals(volume.mPath)); 472 } 473 return false; 474 } 475 476 @Override hashCode()477 public int hashCode() { 478 return mPath.hashCode(); 479 } 480 481 @Override toString()482 public String toString() { 483 final StringBuilder buffer = new StringBuilder("StorageVolume: ").append(mDescription); 484 if (mFsUuid != null) { 485 buffer.append(" (").append(mFsUuid).append(")"); 486 } 487 return buffer.toString(); 488 } 489 490 /** {@hide} */ 491 // TODO: find out where toString() is called internally and replace these calls by dump(). dump()492 public String dump() { 493 final CharArrayWriter writer = new CharArrayWriter(); 494 dump(new IndentingPrintWriter(writer, " ", 80)); 495 return writer.toString(); 496 } 497 498 /** {@hide} */ dump(IndentingPrintWriter pw)499 public void dump(IndentingPrintWriter pw) { 500 pw.println("StorageVolume:"); 501 pw.increaseIndent(); 502 pw.printPair("mId", mId); 503 pw.printPair("mPath", mPath); 504 pw.printPair("mInternalPath", mInternalPath); 505 pw.printPair("mDescription", mDescription); 506 pw.printPair("mPrimary", mPrimary); 507 pw.printPair("mRemovable", mRemovable); 508 pw.printPair("mEmulated", mEmulated); 509 pw.printPair("mAllowMassStorage", mAllowMassStorage); 510 pw.printPair("mMaxFileSize", mMaxFileSize); 511 pw.printPair("mOwner", mOwner); 512 pw.printPair("mFsUuid", mFsUuid); 513 pw.printPair("mState", mState); 514 pw.decreaseIndent(); 515 } 516 517 public static final @android.annotation.NonNull Creator<StorageVolume> CREATOR = new Creator<StorageVolume>() { 518 @Override 519 public StorageVolume createFromParcel(Parcel in) { 520 return new StorageVolume(in); 521 } 522 523 @Override 524 public StorageVolume[] newArray(int size) { 525 return new StorageVolume[size]; 526 } 527 }; 528 529 @Override describeContents()530 public int describeContents() { 531 return 0; 532 } 533 534 @Override writeToParcel(Parcel parcel, int flags)535 public void writeToParcel(Parcel parcel, int flags) { 536 parcel.writeString8(mId); 537 parcel.writeString8(mPath.toString()); 538 parcel.writeString8(mInternalPath.toString()); 539 parcel.writeString8(mDescription); 540 parcel.writeInt(mPrimary ? 1 : 0); 541 parcel.writeInt(mRemovable ? 1 : 0); 542 parcel.writeInt(mEmulated ? 1 : 0); 543 parcel.writeInt(mAllowMassStorage ? 1 : 0); 544 parcel.writeLong(mMaxFileSize); 545 parcel.writeParcelable(mOwner, flags); 546 if (mUuid != null) { 547 parcel.writeInt(1); 548 parcel.writeString8(StorageManager.convert(mUuid)); 549 } else { 550 parcel.writeInt(0); 551 } 552 parcel.writeString8(mFsUuid); 553 parcel.writeString8(mState); 554 } 555 556 /** @hide */ 557 // This class is used by the mainline test suite, so we have to keep these APIs around across 558 // releases. Consider making this class public to help external developers to write tests as 559 // well. 560 @TestApi 561 public static final class Builder { 562 private String mId; 563 private File mPath; 564 private String mDescription; 565 private boolean mPrimary; 566 private boolean mRemovable; 567 private boolean mEmulated; 568 private UserHandle mOwner; 569 private UUID mStorageUuid; 570 private String mUuid; 571 private String mState; 572 573 @SuppressLint("StreamFiles") Builder( @onNull String id, @NonNull File path, @NonNull String description, @NonNull UserHandle owner, @NonNull String state)574 public Builder( 575 @NonNull String id, @NonNull File path, @NonNull String description, 576 @NonNull UserHandle owner, @NonNull String state) { 577 mId = id; 578 mPath = path; 579 mDescription = description; 580 mOwner = owner; 581 mState = state; 582 } 583 584 @NonNull setStorageUuid(@ullable UUID storageUuid)585 public Builder setStorageUuid(@Nullable UUID storageUuid) { 586 mStorageUuid = storageUuid; 587 return this; 588 } 589 590 @NonNull setUuid(@ullable String uuid)591 public Builder setUuid(@Nullable String uuid) { 592 mUuid = uuid; 593 return this; 594 } 595 596 @NonNull setPrimary(boolean primary)597 public Builder setPrimary(boolean primary) { 598 mPrimary = primary; 599 return this; 600 } 601 602 @NonNull setRemovable(boolean removable)603 public Builder setRemovable(boolean removable) { 604 mRemovable = removable; 605 return this; 606 } 607 608 @NonNull setEmulated(boolean emulated)609 public Builder setEmulated(boolean emulated) { 610 mEmulated = emulated; 611 return this; 612 } 613 614 @NonNull build()615 public StorageVolume build() { 616 return new StorageVolume( 617 mId, 618 mPath, 619 /* internalPath= */ mPath, 620 mDescription, 621 mPrimary, 622 mRemovable, 623 mEmulated, 624 /* allowMassStorage= */ false, 625 /* maxFileSize= */ 0, 626 mOwner, 627 mStorageUuid, 628 mUuid, 629 mState); 630 } 631 } 632 633 } 634