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