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.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 @TestApi getPath()177 public String getPath() { 178 return mPath.toString(); 179 } 180 181 /** 182 * Returns the path of the underlying filesystem. 183 * 184 * @return the internal path 185 * @hide 186 */ getInternalPath()187 public String getInternalPath() { 188 return mInternalPath.toString(); 189 } 190 191 /** {@hide} */ 192 @UnsupportedAppUsage getPathFile()193 public File getPathFile() { 194 return mPath; 195 } 196 197 /** 198 * Returns a user-visible description of the volume. 199 * 200 * @return the volume description 201 */ getDescription(Context context)202 public String getDescription(Context context) { 203 return mDescription; 204 } 205 206 /** 207 * Returns true if the volume is the primary shared/external storage, which is the volume 208 * backed by {@link Environment#getExternalStorageDirectory()}. 209 */ isPrimary()210 public boolean isPrimary() { 211 return mPrimary; 212 } 213 214 /** 215 * Returns true if the volume is removable. 216 * 217 * @return is removable 218 */ isRemovable()219 public boolean isRemovable() { 220 return mRemovable; 221 } 222 223 /** 224 * Returns true if the volume is emulated. 225 * 226 * @return is removable 227 */ isEmulated()228 public boolean isEmulated() { 229 return mEmulated; 230 } 231 232 /** 233 * Returns true if this volume can be shared via USB mass storage. 234 * 235 * @return whether mass storage is allowed 236 * @hide 237 */ 238 @UnsupportedAppUsage allowMassStorage()239 public boolean allowMassStorage() { 240 return mAllowMassStorage; 241 } 242 243 /** 244 * Returns maximum file size for the volume, or zero if it is unbounded. 245 * 246 * @return maximum file size 247 * @hide 248 */ 249 @UnsupportedAppUsage getMaxFileSize()250 public long getMaxFileSize() { 251 return mMaxFileSize; 252 } 253 254 /** {@hide} */ 255 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) getOwner()256 public UserHandle getOwner() { 257 return mOwner; 258 } 259 260 /** 261 * Gets the volume UUID, if any. 262 */ getUuid()263 public @Nullable String getUuid() { 264 return mFsUuid; 265 } 266 267 /** {@hide} */ normalizeUuid(@ullable String fsUuid)268 public static @Nullable String normalizeUuid(@Nullable String fsUuid) { 269 return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null; 270 } 271 272 /** {@hide} */ getNormalizedUuid()273 public @Nullable String getNormalizedUuid() { 274 return normalizeUuid(mFsUuid); 275 } 276 277 /** 278 * Parse and return volume UUID as FAT volume ID, or return -1 if unable to 279 * parse or UUID is unknown. 280 * @hide 281 */ 282 @UnsupportedAppUsage getFatVolumeId()283 public int getFatVolumeId() { 284 if (mFsUuid == null || mFsUuid.length() != 9) { 285 return -1; 286 } 287 try { 288 return (int) Long.parseLong(mFsUuid.replace("-", ""), 16); 289 } catch (NumberFormatException e) { 290 return -1; 291 } 292 } 293 294 /** {@hide} */ 295 @UnsupportedAppUsage getUserLabel()296 public String getUserLabel() { 297 return mDescription; 298 } 299 300 /** 301 * Returns the current state of the volume. 302 * 303 * @return one of {@link Environment#MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED}, 304 * {@link Environment#MEDIA_UNMOUNTED}, {@link Environment#MEDIA_CHECKING}, 305 * {@link Environment#MEDIA_NOFS}, {@link Environment#MEDIA_MOUNTED}, 306 * {@link Environment#MEDIA_MOUNTED_READ_ONLY}, {@link Environment#MEDIA_SHARED}, 307 * {@link Environment#MEDIA_BAD_REMOVAL}, or {@link Environment#MEDIA_UNMOUNTABLE}. 308 */ getState()309 public String getState() { 310 return mState; 311 } 312 313 /** 314 * Builds an intent to give access to a standard storage directory or entire volume after 315 * obtaining the user's approval. 316 * <p> 317 * When invoked, the system will ask the user to grant access to the requested directory (and 318 * its descendants). The result of the request will be returned to the activity through the 319 * {@code onActivityResult} method. 320 * <p> 321 * To gain access to descendants (child, grandchild, etc) documents, use 322 * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)}, or 323 * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI. 324 * <p> 325 * If your application only needs to store internal data, consider using 326 * {@link Context#getExternalFilesDirs(String) Context.getExternalFilesDirs}, 327 * {@link Context#getExternalCacheDirs()}, or {@link Context#getExternalMediaDirs()}, which 328 * require no permissions to read or write. 329 * <p> 330 * Access to the entire volume is only available for non-primary volumes (for the primary 331 * volume, apps can use the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and 332 * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions) and should be used 333 * with caution, since users are more likely to deny access when asked for entire volume access 334 * rather than specific directories. 335 * 336 * @param directoryName must be one of {@link Environment#DIRECTORY_MUSIC}, 337 * {@link Environment#DIRECTORY_PODCASTS}, {@link Environment#DIRECTORY_RINGTONES}, 338 * {@link Environment#DIRECTORY_ALARMS}, {@link Environment#DIRECTORY_NOTIFICATIONS}, 339 * {@link Environment#DIRECTORY_PICTURES}, {@link Environment#DIRECTORY_MOVIES}, 340 * {@link Environment#DIRECTORY_DOWNLOADS}, {@link Environment#DIRECTORY_DCIM}, or 341 * {@link Environment#DIRECTORY_DOCUMENTS}, or {@code null} to request access to the 342 * entire volume. 343 * @return intent to request access, or {@code null} if the requested directory is invalid for 344 * that volume. 345 * @see DocumentsContract 346 * @deprecated Callers should migrate to using {@link Intent#ACTION_OPEN_DOCUMENT_TREE} instead. 347 * Launching this {@link Intent} on devices running 348 * {@link android.os.Build.VERSION_CODES#Q} or higher, will immediately finish 349 * with a result code of {@link android.app.Activity#RESULT_CANCELED}. 350 */ 351 @Deprecated createAccessIntent(String directoryName)352 public @Nullable Intent createAccessIntent(String directoryName) { 353 if ((isPrimary() && directoryName == null) || 354 (directoryName != null && !Environment.isStandardDirectory(directoryName))) { 355 return null; 356 } 357 final Intent intent = new Intent(ACTION_OPEN_EXTERNAL_DIRECTORY); 358 intent.putExtra(EXTRA_STORAGE_VOLUME, this); 359 intent.putExtra(EXTRA_DIRECTORY_NAME, directoryName); 360 return intent; 361 } 362 363 /** 364 * Builds an {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to allow the user to grant access to any 365 * directory subtree (or entire volume) from the {@link android.provider.DocumentsProvider}s 366 * available on the device. The initial location of the document navigation will be the root of 367 * this {@link StorageVolume}. 368 * 369 * Note that the returned {@link Intent} simply suggests that the user picks this {@link 370 * StorageVolume} by default, but the user may select a different location. Callers must respect 371 * the user's chosen location, even if it is different from the originally requested location. 372 * 373 * @return intent to {@link Intent#ACTION_OPEN_DOCUMENT_TREE} initially showing the contents 374 * of this {@link StorageVolume} 375 * @see Intent#ACTION_OPEN_DOCUMENT_TREE 376 */ createOpenDocumentTreeIntent()377 @NonNull public Intent createOpenDocumentTreeIntent() { 378 final String rootId = isEmulated() 379 ? DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID 380 : mFsUuid; 381 final Uri rootUri = DocumentsContract.buildRootUri( 382 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, rootId); 383 final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 384 .putExtra(DocumentsContract.EXTRA_INITIAL_URI, rootUri) 385 .putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, true); 386 return intent; 387 } 388 389 @Override equals(Object obj)390 public boolean equals(Object obj) { 391 if (obj instanceof StorageVolume && mPath != null) { 392 StorageVolume volume = (StorageVolume)obj; 393 return (mPath.equals(volume.mPath)); 394 } 395 return false; 396 } 397 398 @Override hashCode()399 public int hashCode() { 400 return mPath.hashCode(); 401 } 402 403 @Override toString()404 public String toString() { 405 final StringBuilder buffer = new StringBuilder("StorageVolume: ").append(mDescription); 406 if (mFsUuid != null) { 407 buffer.append(" (").append(mFsUuid).append(")"); 408 } 409 return buffer.toString(); 410 } 411 412 /** {@hide} */ 413 // TODO: find out where toString() is called internally and replace these calls by dump(). dump()414 public String dump() { 415 final CharArrayWriter writer = new CharArrayWriter(); 416 dump(new IndentingPrintWriter(writer, " ", 80)); 417 return writer.toString(); 418 } 419 420 /** {@hide} */ dump(IndentingPrintWriter pw)421 public void dump(IndentingPrintWriter pw) { 422 pw.println("StorageVolume:"); 423 pw.increaseIndent(); 424 pw.printPair("mId", mId); 425 pw.printPair("mPath", mPath); 426 pw.printPair("mInternalPath", mInternalPath); 427 pw.printPair("mDescription", mDescription); 428 pw.printPair("mPrimary", mPrimary); 429 pw.printPair("mRemovable", mRemovable); 430 pw.printPair("mEmulated", mEmulated); 431 pw.printPair("mAllowMassStorage", mAllowMassStorage); 432 pw.printPair("mMaxFileSize", mMaxFileSize); 433 pw.printPair("mOwner", mOwner); 434 pw.printPair("mFsUuid", mFsUuid); 435 pw.printPair("mState", mState); 436 pw.decreaseIndent(); 437 } 438 439 public static final @android.annotation.NonNull Creator<StorageVolume> CREATOR = new Creator<StorageVolume>() { 440 @Override 441 public StorageVolume createFromParcel(Parcel in) { 442 return new StorageVolume(in); 443 } 444 445 @Override 446 public StorageVolume[] newArray(int size) { 447 return new StorageVolume[size]; 448 } 449 }; 450 451 @Override describeContents()452 public int describeContents() { 453 return 0; 454 } 455 456 @Override writeToParcel(Parcel parcel, int flags)457 public void writeToParcel(Parcel parcel, int flags) { 458 parcel.writeString(mId); 459 parcel.writeString(mPath.toString()); 460 parcel.writeString(mInternalPath.toString()); 461 parcel.writeString(mDescription); 462 parcel.writeInt(mPrimary ? 1 : 0); 463 parcel.writeInt(mRemovable ? 1 : 0); 464 parcel.writeInt(mEmulated ? 1 : 0); 465 parcel.writeInt(mAllowMassStorage ? 1 : 0); 466 parcel.writeLong(mMaxFileSize); 467 parcel.writeParcelable(mOwner, flags); 468 parcel.writeString(mFsUuid); 469 parcel.writeString(mState); 470 } 471 } 472