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