1 /* 2 * Copyright (C) 2015 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.content.Context; 22 import android.content.Intent; 23 import android.content.res.Resources; 24 import android.net.Uri; 25 import android.os.Environment; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.os.UserHandle; 29 import android.provider.DocumentsContract; 30 import android.text.TextUtils; 31 import android.util.ArrayMap; 32 import android.util.DebugUtils; 33 import android.util.SparseArray; 34 import android.util.SparseIntArray; 35 36 import com.android.internal.R; 37 import com.android.internal.util.IndentingPrintWriter; 38 import com.android.internal.util.Preconditions; 39 40 import java.io.CharArrayWriter; 41 import java.io.File; 42 import java.util.Comparator; 43 import java.util.Objects; 44 45 /** 46 * Information about a storage volume that may be mounted. A volume may be a 47 * partition on a physical {@link DiskInfo}, an emulated volume above some other 48 * storage medium, or a standalone container like an ASEC or OBB. 49 * <p> 50 * Volumes may be mounted with various flags: 51 * <ul> 52 * <li>{@link #MOUNT_FLAG_PRIMARY} means the volume provides primary external 53 * storage, historically found at {@code /sdcard}. 54 * <li>{@link #MOUNT_FLAG_VISIBLE} means the volume is visible to third-party 55 * apps for direct filesystem access. The system should send out relevant 56 * storage broadcasts and index any media on visible volumes. Visible volumes 57 * are considered a more stable part of the device, which is why we take the 58 * time to index them. In particular, transient volumes like USB OTG devices 59 * <em>should not</em> be marked as visible; their contents should be surfaced 60 * to apps through the Storage Access Framework. 61 * </ul> 62 * 63 * @hide 64 */ 65 public class VolumeInfo implements Parcelable { 66 public static final String ACTION_VOLUME_STATE_CHANGED = 67 "android.os.storage.action.VOLUME_STATE_CHANGED"; 68 public static final String EXTRA_VOLUME_ID = 69 "android.os.storage.extra.VOLUME_ID"; 70 public static final String EXTRA_VOLUME_STATE = 71 "android.os.storage.extra.VOLUME_STATE"; 72 73 /** Stub volume representing internal private storage */ 74 public static final String ID_PRIVATE_INTERNAL = "private"; 75 /** Real volume representing internal emulated storage */ 76 public static final String ID_EMULATED_INTERNAL = "emulated"; 77 78 public static final int TYPE_PUBLIC = 0; 79 public static final int TYPE_PRIVATE = 1; 80 public static final int TYPE_EMULATED = 2; 81 public static final int TYPE_ASEC = 3; 82 public static final int TYPE_OBB = 4; 83 84 public static final int STATE_UNMOUNTED = 0; 85 public static final int STATE_CHECKING = 1; 86 public static final int STATE_MOUNTED = 2; 87 public static final int STATE_MOUNTED_READ_ONLY = 3; 88 public static final int STATE_FORMATTING = 4; 89 public static final int STATE_EJECTING = 5; 90 public static final int STATE_UNMOUNTABLE = 6; 91 public static final int STATE_REMOVED = 7; 92 public static final int STATE_BAD_REMOVAL = 8; 93 94 public static final int MOUNT_FLAG_PRIMARY = 1 << 0; 95 public static final int MOUNT_FLAG_VISIBLE = 1 << 1; 96 97 private static SparseArray<String> sStateToEnvironment = new SparseArray<>(); 98 private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>(); 99 private static SparseIntArray sStateToDescrip = new SparseIntArray(); 100 101 private static final Comparator<VolumeInfo> 102 sDescriptionComparator = new Comparator<VolumeInfo>() { 103 @Override 104 public int compare(VolumeInfo lhs, VolumeInfo rhs) { 105 if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(lhs.getId())) { 106 return -1; 107 } else if (lhs.getDescription() == null) { 108 return 1; 109 } else if (rhs.getDescription() == null) { 110 return -1; 111 } else { 112 return lhs.getDescription().compareTo(rhs.getDescription()); 113 } 114 } 115 }; 116 117 static { 118 sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTED, Environment.MEDIA_UNMOUNTED); 119 sStateToEnvironment.put(VolumeInfo.STATE_CHECKING, Environment.MEDIA_CHECKING); 120 sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED, Environment.MEDIA_MOUNTED); 121 sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, Environment.MEDIA_MOUNTED_READ_ONLY); 122 sStateToEnvironment.put(VolumeInfo.STATE_FORMATTING, Environment.MEDIA_UNMOUNTED); 123 sStateToEnvironment.put(VolumeInfo.STATE_EJECTING, Environment.MEDIA_EJECTING); 124 sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTABLE, Environment.MEDIA_UNMOUNTABLE); 125 sStateToEnvironment.put(VolumeInfo.STATE_REMOVED, Environment.MEDIA_REMOVED); 126 sStateToEnvironment.put(VolumeInfo.STATE_BAD_REMOVAL, Environment.MEDIA_BAD_REMOVAL); 127 128 sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTED, Intent.ACTION_MEDIA_UNMOUNTED); 129 sEnvironmentToBroadcast.put(Environment.MEDIA_CHECKING, Intent.ACTION_MEDIA_CHECKING); 130 sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED, Intent.ACTION_MEDIA_MOUNTED); 131 sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED_READ_ONLY, Intent.ACTION_MEDIA_MOUNTED); 132 sEnvironmentToBroadcast.put(Environment.MEDIA_EJECTING, Intent.ACTION_MEDIA_EJECT); 133 sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTABLE, Intent.ACTION_MEDIA_UNMOUNTABLE); 134 sEnvironmentToBroadcast.put(Environment.MEDIA_REMOVED, Intent.ACTION_MEDIA_REMOVED); 135 sEnvironmentToBroadcast.put(Environment.MEDIA_BAD_REMOVAL, Intent.ACTION_MEDIA_BAD_REMOVAL); 136 137 sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTED, R.string.ext_media_status_unmounted); 138 sStateToDescrip.put(VolumeInfo.STATE_CHECKING, R.string.ext_media_status_checking); 139 sStateToDescrip.put(VolumeInfo.STATE_MOUNTED, R.string.ext_media_status_mounted); 140 sStateToDescrip.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, R.string.ext_media_status_mounted_ro); 141 sStateToDescrip.put(VolumeInfo.STATE_FORMATTING, R.string.ext_media_status_formatting); 142 sStateToDescrip.put(VolumeInfo.STATE_EJECTING, R.string.ext_media_status_ejecting); 143 sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTABLE, R.string.ext_media_status_unmountable); 144 sStateToDescrip.put(VolumeInfo.STATE_REMOVED, R.string.ext_media_status_removed); 145 sStateToDescrip.put(VolumeInfo.STATE_BAD_REMOVAL, R.string.ext_media_status_bad_removal); 146 } 147 148 /** vold state */ 149 public final String id; 150 public final int type; 151 public final DiskInfo disk; 152 public final String partGuid; 153 public int mountFlags = 0; 154 public int mountUserId = -1; 155 public int state = STATE_UNMOUNTED; 156 public String fsType; 157 public String fsUuid; 158 public String fsLabel; 159 public String path; 160 public String internalPath; 161 162 public VolumeInfo(String id, int type, DiskInfo disk, String partGuid) { 163 this.id = Preconditions.checkNotNull(id); 164 this.type = type; 165 this.disk = disk; 166 this.partGuid = partGuid; 167 } 168 169 public VolumeInfo(Parcel parcel) { 170 id = parcel.readString(); 171 type = parcel.readInt(); 172 if (parcel.readInt() != 0) { 173 disk = DiskInfo.CREATOR.createFromParcel(parcel); 174 } else { 175 disk = null; 176 } 177 partGuid = parcel.readString(); 178 mountFlags = parcel.readInt(); 179 mountUserId = parcel.readInt(); 180 state = parcel.readInt(); 181 fsType = parcel.readString(); 182 fsUuid = parcel.readString(); 183 fsLabel = parcel.readString(); 184 path = parcel.readString(); 185 internalPath = parcel.readString(); 186 } 187 188 public static @NonNull String getEnvironmentForState(int state) { 189 final String envState = sStateToEnvironment.get(state); 190 if (envState != null) { 191 return envState; 192 } else { 193 return Environment.MEDIA_UNKNOWN; 194 } 195 } 196 197 public static @Nullable String getBroadcastForEnvironment(String envState) { 198 return sEnvironmentToBroadcast.get(envState); 199 } 200 201 public static @Nullable String getBroadcastForState(int state) { 202 return getBroadcastForEnvironment(getEnvironmentForState(state)); 203 } 204 205 public static @NonNull Comparator<VolumeInfo> getDescriptionComparator() { 206 return sDescriptionComparator; 207 } 208 209 public @NonNull String getId() { 210 return id; 211 } 212 213 public @Nullable DiskInfo getDisk() { 214 return disk; 215 } 216 217 public @Nullable String getDiskId() { 218 return (disk != null) ? disk.id : null; 219 } 220 221 public int getType() { 222 return type; 223 } 224 225 public int getState() { 226 return state; 227 } 228 229 public int getStateDescription() { 230 return sStateToDescrip.get(state, 0); 231 } 232 233 public @Nullable String getFsUuid() { 234 return fsUuid; 235 } 236 237 public int getMountUserId() { 238 return mountUserId; 239 } 240 241 public @Nullable String getDescription() { 242 if (ID_PRIVATE_INTERNAL.equals(id) || ID_EMULATED_INTERNAL.equals(id)) { 243 return Resources.getSystem().getString(com.android.internal.R.string.storage_internal); 244 } else if (!TextUtils.isEmpty(fsLabel)) { 245 return fsLabel; 246 } else { 247 return null; 248 } 249 } 250 251 public boolean isMountedReadable() { 252 return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY; 253 } 254 255 public boolean isMountedWritable() { 256 return state == STATE_MOUNTED; 257 } 258 259 public boolean isPrimary() { 260 return (mountFlags & MOUNT_FLAG_PRIMARY) != 0; 261 } 262 263 public boolean isPrimaryPhysical() { 264 return isPrimary() && (getType() == TYPE_PUBLIC); 265 } 266 267 public boolean isVisible() { 268 return (mountFlags & MOUNT_FLAG_VISIBLE) != 0; 269 } 270 271 public boolean isVisibleForRead(int userId) { 272 if (type == TYPE_PUBLIC) { 273 if (isPrimary() && mountUserId != userId) { 274 // Primary physical is only visible to single user 275 return false; 276 } else { 277 return isVisible(); 278 } 279 } else if (type == TYPE_EMULATED) { 280 return isVisible(); 281 } else { 282 return false; 283 } 284 } 285 286 public boolean isVisibleForWrite(int userId) { 287 if (type == TYPE_PUBLIC && mountUserId == userId) { 288 return isVisible(); 289 } else if (type == TYPE_EMULATED) { 290 return isVisible(); 291 } else { 292 return false; 293 } 294 } 295 296 public File getPath() { 297 return (path != null) ? new File(path) : null; 298 } 299 300 public File getInternalPath() { 301 return (internalPath != null) ? new File(internalPath) : null; 302 } 303 304 public File getPathForUser(int userId) { 305 if (path == null) { 306 return null; 307 } else if (type == TYPE_PUBLIC) { 308 return new File(path); 309 } else if (type == TYPE_EMULATED) { 310 return new File(path, Integer.toString(userId)); 311 } else { 312 return null; 313 } 314 } 315 316 /** 317 * Path which is accessible to apps holding 318 * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}. 319 */ 320 public File getInternalPathForUser(int userId) { 321 if (type == TYPE_PUBLIC) { 322 // TODO: plumb through cleaner path from vold 323 return new File(path.replace("/storage/", "/mnt/media_rw/")); 324 } else { 325 return getPathForUser(userId); 326 } 327 } 328 329 public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) { 330 final StorageManager storage = context.getSystemService(StorageManager.class); 331 332 final boolean removable; 333 final boolean emulated; 334 final boolean allowMassStorage = false; 335 final String envState = reportUnmounted 336 ? Environment.MEDIA_UNMOUNTED : getEnvironmentForState(state); 337 338 File userPath = getPathForUser(userId); 339 if (userPath == null) { 340 userPath = new File("/dev/null"); 341 } 342 343 String description = null; 344 String derivedFsUuid = fsUuid; 345 long mtpReserveSize = 0; 346 long maxFileSize = 0; 347 int mtpStorageId = StorageVolume.STORAGE_ID_INVALID; 348 349 if (type == TYPE_EMULATED) { 350 emulated = true; 351 352 final VolumeInfo privateVol = storage.findPrivateForEmulated(this); 353 if (privateVol != null) { 354 description = storage.getBestVolumeDescription(privateVol); 355 derivedFsUuid = privateVol.fsUuid; 356 } 357 358 if (isPrimary()) { 359 mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY; 360 } 361 362 mtpReserveSize = storage.getStorageLowBytes(userPath); 363 364 if (ID_EMULATED_INTERNAL.equals(id)) { 365 removable = false; 366 } else { 367 removable = true; 368 } 369 370 } else if (type == TYPE_PUBLIC) { 371 emulated = false; 372 removable = true; 373 374 description = storage.getBestVolumeDescription(this); 375 376 if (isPrimary()) { 377 mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY; 378 } else { 379 // Since MediaProvider currently persists this value, we need a 380 // value that is stable over time. 381 mtpStorageId = buildStableMtpStorageId(fsUuid); 382 } 383 384 if ("vfat".equals(fsType)) { 385 maxFileSize = 4294967295L; 386 } 387 388 } else { 389 throw new IllegalStateException("Unexpected volume type " + type); 390 } 391 392 if (description == null) { 393 description = context.getString(android.R.string.unknownName); 394 } 395 396 return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable, 397 emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId), 398 derivedFsUuid, envState); 399 } 400 401 public static int buildStableMtpStorageId(String fsUuid) { 402 if (TextUtils.isEmpty(fsUuid)) { 403 return StorageVolume.STORAGE_ID_INVALID; 404 } else { 405 int hash = 0; 406 for (int i = 0; i < fsUuid.length(); ++i) { 407 hash = 31 * hash + fsUuid.charAt(i); 408 } 409 hash = (hash ^ (hash << 16)) & 0xffff0000; 410 // Work around values that the spec doesn't allow, or that we've 411 // reserved for primary 412 if (hash == 0x00000000) hash = 0x00020000; 413 if (hash == 0x00010000) hash = 0x00020000; 414 if (hash == 0xffff0000) hash = 0xfffe0000; 415 return hash | 0x0001; 416 } 417 } 418 419 // TODO: avoid this layering violation 420 private static final String DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; 421 private static final String DOCUMENT_ROOT_PRIMARY_EMULATED = "primary"; 422 423 /** 424 * Build an intent to browse the contents of this volume. Only valid for 425 * {@link #TYPE_EMULATED} or {@link #TYPE_PUBLIC}. 426 */ 427 public Intent buildBrowseIntent() { 428 final Uri uri; 429 if (type == VolumeInfo.TYPE_PUBLIC) { 430 uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, fsUuid); 431 } else if (type == VolumeInfo.TYPE_EMULATED && isPrimary()) { 432 uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, 433 DOCUMENT_ROOT_PRIMARY_EMULATED); 434 } else { 435 return null; 436 } 437 438 final Intent intent = new Intent(DocumentsContract.ACTION_BROWSE); 439 intent.addCategory(Intent.CATEGORY_DEFAULT); 440 intent.setData(uri); 441 442 // note that docsui treats this as *force* show advanced. So sending 443 // false permits advanced to be shown based on user preferences. 444 intent.putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, isPrimary()); 445 intent.putExtra(DocumentsContract.EXTRA_FANCY_FEATURES, true); 446 intent.putExtra(DocumentsContract.EXTRA_SHOW_FILESIZE, true); 447 return intent; 448 } 449 450 @Override 451 public String toString() { 452 final CharArrayWriter writer = new CharArrayWriter(); 453 dump(new IndentingPrintWriter(writer, " ", 80)); 454 return writer.toString(); 455 } 456 457 public void dump(IndentingPrintWriter pw) { 458 pw.println("VolumeInfo{" + id + "}:"); 459 pw.increaseIndent(); 460 pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type)); 461 pw.printPair("diskId", getDiskId()); 462 pw.printPair("partGuid", partGuid); 463 pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags)); 464 pw.printPair("mountUserId", mountUserId); 465 pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state)); 466 pw.println(); 467 pw.printPair("fsType", fsType); 468 pw.printPair("fsUuid", fsUuid); 469 pw.printPair("fsLabel", fsLabel); 470 pw.println(); 471 pw.printPair("path", path); 472 pw.printPair("internalPath", internalPath); 473 pw.decreaseIndent(); 474 pw.println(); 475 } 476 477 @Override 478 public VolumeInfo clone() { 479 final Parcel temp = Parcel.obtain(); 480 try { 481 writeToParcel(temp, 0); 482 temp.setDataPosition(0); 483 return CREATOR.createFromParcel(temp); 484 } finally { 485 temp.recycle(); 486 } 487 } 488 489 @Override 490 public boolean equals(Object o) { 491 if (o instanceof VolumeInfo) { 492 return Objects.equals(id, ((VolumeInfo) o).id); 493 } else { 494 return false; 495 } 496 } 497 498 @Override 499 public int hashCode() { 500 return id.hashCode(); 501 } 502 503 public static final Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() { 504 @Override 505 public VolumeInfo createFromParcel(Parcel in) { 506 return new VolumeInfo(in); 507 } 508 509 @Override 510 public VolumeInfo[] newArray(int size) { 511 return new VolumeInfo[size]; 512 } 513 }; 514 515 @Override 516 public int describeContents() { 517 return 0; 518 } 519 520 @Override 521 public void writeToParcel(Parcel parcel, int flags) { 522 parcel.writeString(id); 523 parcel.writeInt(type); 524 if (disk != null) { 525 parcel.writeInt(1); 526 disk.writeToParcel(parcel, flags); 527 } else { 528 parcel.writeInt(0); 529 } 530 parcel.writeString(partGuid); 531 parcel.writeInt(mountFlags); 532 parcel.writeInt(mountUserId); 533 parcel.writeInt(state); 534 parcel.writeString(fsType); 535 parcel.writeString(fsUuid); 536 parcel.writeString(fsLabel); 537 parcel.writeString(path); 538 parcel.writeString(internalPath); 539 } 540 } 541