2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.os.storage;
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.IVold;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.os.UserHandle;
30 import android.provider.DocumentsContract;
31 import android.text.TextUtils;
32 import android.util.ArrayMap;
33 import android.util.DebugUtils;
34 import android.util.SparseArray;
35 import android.util.SparseIntArray;
37 import com.android.internal.R;
38 import com.android.internal.util.IndentingPrintWriter;
39 import com.android.internal.util.Preconditions;
41 import java.io.CharArrayWriter;
43 import java.util.Comparator;
44 import java.util.Objects;
47 * Information about a storage volume that may be mounted. A volume may be a
48 * partition on a physical {@link DiskInfo}, an emulated volume above some other
49 * storage medium, or a standalone container like an ASEC or OBB.
51 * Volumes may be mounted with various flags:
53 * <li>{@link #MOUNT_FLAG_PRIMARY} means the volume provides primary external
54 * storage, historically found at {@code /sdcard}.
55 * <li>{@link #MOUNT_FLAG_VISIBLE} means the volume is visible to third-party
56 * apps for direct filesystem access. The system should send out relevant
57 * storage broadcasts and index any media on visible volumes. Visible volumes
58 * are considered a more stable part of the device, which is why we take the
59 * time to index them. In particular, transient volumes like USB OTG devices
60 * <em>should not</em> be marked as visible; their contents should be surfaced
61 * to apps through the Storage Access Framework.
66 public class VolumeInfo implements Parcelable {
67 public static final String ACTION_VOLUME_STATE_CHANGED =
68 "android.os.storage.action.VOLUME_STATE_CHANGED";
69 public static final String EXTRA_VOLUME_ID =
70 "android.os.storage.extra.VOLUME_ID";
71 public static final String EXTRA_VOLUME_STATE =
72 "android.os.storage.extra.VOLUME_STATE";
74 /** Stub volume representing internal private storage */
75 public static final String ID_PRIVATE_INTERNAL = "private";
76 /** Real volume representing internal emulated storage */
77 public static final String ID_EMULATED_INTERNAL = "emulated";
79 public static final int TYPE_PUBLIC = IVold.VOLUME_TYPE_PUBLIC;
80 public static final int TYPE_PRIVATE = IVold.VOLUME_TYPE_PRIVATE;
81 public static final int TYPE_EMULATED = IVold.VOLUME_TYPE_EMULATED;
82 public static final int TYPE_ASEC = IVold.VOLUME_TYPE_ASEC;
83 public static final int TYPE_OBB = IVold.VOLUME_TYPE_OBB;
85 public static final int STATE_UNMOUNTED = IVold.VOLUME_STATE_UNMOUNTED;
86 public static final int STATE_CHECKING = IVold.VOLUME_STATE_CHECKING;
87 public static final int STATE_MOUNTED = IVold.VOLUME_STATE_MOUNTED;
88 public static final int STATE_MOUNTED_READ_ONLY = IVold.VOLUME_STATE_MOUNTED_READ_ONLY;
89 public static final int STATE_FORMATTING = IVold.VOLUME_STATE_FORMATTING;
90 public static final int STATE_EJECTING = IVold.VOLUME_STATE_EJECTING;
91 public static final int STATE_UNMOUNTABLE = IVold.VOLUME_STATE_UNMOUNTABLE;
92 public static final int STATE_REMOVED = IVold.VOLUME_STATE_REMOVED;
93 public static final int STATE_BAD_REMOVAL = IVold.VOLUME_STATE_BAD_REMOVAL;
95 public static final int MOUNT_FLAG_PRIMARY = IVold.MOUNT_FLAG_PRIMARY;
96 public static final int MOUNT_FLAG_VISIBLE = IVold.MOUNT_FLAG_VISIBLE;
98 private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
99 private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
100 private static SparseIntArray sStateToDescrip = new SparseIntArray();
102 private static final Comparator<VolumeInfo>
103 sDescriptionComparator = new Comparator<VolumeInfo>() {
105 public int compare(VolumeInfo lhs, VolumeInfo rhs) {
106 if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(lhs.getId())) {
108 } else if (lhs.getDescription() == null) {
110 } else if (rhs.getDescription() == null) {
113 return lhs.getDescription().compareTo(rhs.getDescription());
119 sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTED, Environment.MEDIA_UNMOUNTED);
120 sStateToEnvironment.put(VolumeInfo.STATE_CHECKING, Environment.MEDIA_CHECKING);
121 sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED, Environment.MEDIA_MOUNTED);
122 sStateToEnvironment.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, Environment.MEDIA_MOUNTED_READ_ONLY);
123 sStateToEnvironment.put(VolumeInfo.STATE_FORMATTING, Environment.MEDIA_UNMOUNTED);
124 sStateToEnvironment.put(VolumeInfo.STATE_EJECTING, Environment.MEDIA_EJECTING);
125 sStateToEnvironment.put(VolumeInfo.STATE_UNMOUNTABLE, Environment.MEDIA_UNMOUNTABLE);
126 sStateToEnvironment.put(VolumeInfo.STATE_REMOVED, Environment.MEDIA_REMOVED);
127 sStateToEnvironment.put(VolumeInfo.STATE_BAD_REMOVAL, Environment.MEDIA_BAD_REMOVAL);
129 sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTED, Intent.ACTION_MEDIA_UNMOUNTED);
130 sEnvironmentToBroadcast.put(Environment.MEDIA_CHECKING, Intent.ACTION_MEDIA_CHECKING);
131 sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED, Intent.ACTION_MEDIA_MOUNTED);
132 sEnvironmentToBroadcast.put(Environment.MEDIA_MOUNTED_READ_ONLY, Intent.ACTION_MEDIA_MOUNTED);
133 sEnvironmentToBroadcast.put(Environment.MEDIA_EJECTING, Intent.ACTION_MEDIA_EJECT);
134 sEnvironmentToBroadcast.put(Environment.MEDIA_UNMOUNTABLE, Intent.ACTION_MEDIA_UNMOUNTABLE);
135 sEnvironmentToBroadcast.put(Environment.MEDIA_REMOVED, Intent.ACTION_MEDIA_REMOVED);
136 sEnvironmentToBroadcast.put(Environment.MEDIA_BAD_REMOVAL, Intent.ACTION_MEDIA_BAD_REMOVAL);
138 sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTED, R.string.ext_media_status_unmounted);
139 sStateToDescrip.put(VolumeInfo.STATE_CHECKING, R.string.ext_media_status_checking);
140 sStateToDescrip.put(VolumeInfo.STATE_MOUNTED, R.string.ext_media_status_mounted);
141 sStateToDescrip.put(VolumeInfo.STATE_MOUNTED_READ_ONLY, R.string.ext_media_status_mounted_ro);
142 sStateToDescrip.put(VolumeInfo.STATE_FORMATTING, R.string.ext_media_status_formatting);
143 sStateToDescrip.put(VolumeInfo.STATE_EJECTING, R.string.ext_media_status_ejecting);
144 sStateToDescrip.put(VolumeInfo.STATE_UNMOUNTABLE, R.string.ext_media_status_unmountable);
145 sStateToDescrip.put(VolumeInfo.STATE_REMOVED, R.string.ext_media_status_removed);
146 sStateToDescrip.put(VolumeInfo.STATE_BAD_REMOVAL, R.string.ext_media_status_bad_removal);
150 public final String id;
151 public final int type;
152 public final DiskInfo disk;
153 public final String partGuid;
154 public int mountFlags = 0;
155 public int mountUserId = -1;
156 public int state = STATE_UNMOUNTED;
157 public String fsType;
158 public String fsUuid;
159 public String fsLabel;
161 public String internalPath;
163 public VolumeInfo(String id, int type, DiskInfo disk, String partGuid) {
164 this.id = Preconditions.checkNotNull(id);
167 this.partGuid = partGuid;
170 public VolumeInfo(Parcel parcel) {
171 id = parcel.readString();
172 type = parcel.readInt();
173 if (parcel.readInt() != 0) {
174 disk = DiskInfo.CREATOR.createFromParcel(parcel);
178 partGuid = parcel.readString();
179 mountFlags = parcel.readInt();
180 mountUserId = parcel.readInt();
181 state = parcel.readInt();
182 fsType = parcel.readString();
183 fsUuid = parcel.readString();
184 fsLabel = parcel.readString();
185 path = parcel.readString();
186 internalPath = parcel.readString();
189 public static @NonNull String getEnvironmentForState(int state) {
190 final String envState = sStateToEnvironment.get(state);
191 if (envState != null) {
194 return Environment.MEDIA_UNKNOWN;
198 public static @Nullable String getBroadcastForEnvironment(String envState) {
199 return sEnvironmentToBroadcast.get(envState);
202 public static @Nullable String getBroadcastForState(int state) {
203 return getBroadcastForEnvironment(getEnvironmentForState(state));
206 public static @NonNull Comparator<VolumeInfo> getDescriptionComparator() {
207 return sDescriptionComparator;
210 public @NonNull String getId() {
214 public @Nullable DiskInfo getDisk() {
218 public @Nullable String getDiskId() {
219 return (disk != null) ? disk.id : null;
222 public int getType() {
226 public int getState() {
230 public int getStateDescription() {
231 return sStateToDescrip.get(state, 0);
234 public @Nullable String getFsUuid() {
238 public int getMountUserId() {
242 public @Nullable String getDescription() {
243 if (ID_PRIVATE_INTERNAL.equals(id) || ID_EMULATED_INTERNAL.equals(id)) {
244 return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
245 } else if (!TextUtils.isEmpty(fsLabel)) {
252 public boolean isMountedReadable() {
253 return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY;
256 public boolean isMountedWritable() {
257 return state == STATE_MOUNTED;
260 public boolean isPrimary() {
261 return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
264 public boolean isPrimaryPhysical() {
265 return isPrimary() && (getType() == TYPE_PUBLIC);
268 public boolean isVisible() {
269 return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
272 public boolean isVisibleForUser(int userId) {
273 if (type == TYPE_PUBLIC && mountUserId == userId) {
275 } else if (type == TYPE_EMULATED) {
282 public boolean isVisibleForRead(int userId) {
283 return isVisibleForUser(userId);
286 public boolean isVisibleForWrite(int userId) {
287 return isVisibleForUser(userId);
290 public File getPath() {
291 return (path != null) ? new File(path) : null;
294 public File getInternalPath() {
295 return (internalPath != null) ? new File(internalPath) : null;
298 public File getPathForUser(int userId) {
301 } else if (type == TYPE_PUBLIC) {
302 return new File(path);
303 } else if (type == TYPE_EMULATED) {
304 return new File(path, Integer.toString(userId));
311 * Path which is accessible to apps holding
312 * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}.
314 public File getInternalPathForUser(int userId) {
315 if (type == TYPE_PUBLIC) {
316 // TODO: plumb through cleaner path from vold
317 return new File(path.replace("/storage/", "/mnt/media_rw/"));
319 return getPathForUser(userId);
323 public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
324 final StorageManager storage = context.getSystemService(StorageManager.class);
326 final boolean removable;
327 final boolean emulated;
328 final boolean allowMassStorage = false;
329 final String envState = reportUnmounted
330 ? Environment.MEDIA_UNMOUNTED : getEnvironmentForState(state);
332 File userPath = getPathForUser(userId);
333 if (userPath == null) {
334 userPath = new File("/dev/null");
336 File internalPath = getInternalPathForUser(userId);
337 if (internalPath == null) {
338 internalPath = new File("/dev/null");
341 String description = null;
342 String derivedFsUuid = fsUuid;
343 long maxFileSize = 0;
345 if (type == TYPE_EMULATED) {
348 final VolumeInfo privateVol = storage.findPrivateForEmulated(this);
349 if (privateVol != null) {
350 description = storage.getBestVolumeDescription(privateVol);
351 derivedFsUuid = privateVol.fsUuid;
354 if (ID_EMULATED_INTERNAL.equals(id)) {
360 } else if (type == TYPE_PUBLIC) {
364 description = storage.getBestVolumeDescription(this);
366 if ("vfat".equals(fsType)) {
367 maxFileSize = 4294967295L;
371 throw new IllegalStateException("Unexpected volume type " + type);
374 if (description == null) {
375 description = context.getString(android.R.string.unknownName);
378 return new StorageVolume(id, userPath, internalPath, description, isPrimary(), removable,
379 emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
380 derivedFsUuid, envState);
383 public static int buildStableMtpStorageId(String fsUuid) {
384 if (TextUtils.isEmpty(fsUuid)) {
385 return StorageVolume.STORAGE_ID_INVALID;
388 for (int i = 0; i < fsUuid.length(); ++i) {
389 hash = 31 * hash + fsUuid.charAt(i);
391 hash = (hash ^ (hash << 16)) & 0xffff0000;
392 // Work around values that the spec doesn't allow, or that we've
393 // reserved for primary
394 if (hash == 0x00000000) hash = 0x00020000;
395 if (hash == 0x00010000) hash = 0x00020000;
396 if (hash == 0xffff0000) hash = 0xfffe0000;
397 return hash | 0x0001;
401 // TODO: avoid this layering violation
402 private static final String DOCUMENT_AUTHORITY = "com.android.externalstorage.documents";
403 private static final String DOCUMENT_ROOT_PRIMARY_EMULATED = "primary";
406 * Build an intent to browse the contents of this volume. Only valid for
407 * {@link #TYPE_EMULATED} or {@link #TYPE_PUBLIC}.
409 public @Nullable Intent buildBrowseIntent() {
410 return buildBrowseIntentForUser(UserHandle.myUserId());
413 public @Nullable Intent buildBrowseIntentForUser(int userId) {
415 if (type == VolumeInfo.TYPE_PUBLIC && mountUserId == userId) {
416 uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, fsUuid);
417 } else if (type == VolumeInfo.TYPE_EMULATED && isPrimary()) {
418 uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY,
419 DOCUMENT_ROOT_PRIMARY_EMULATED);
424 final Intent intent = new Intent(Intent.ACTION_VIEW);
425 intent.addCategory(Intent.CATEGORY_DEFAULT);
426 intent.setDataAndType(uri, DocumentsContract.Root.MIME_TYPE_ITEM);
428 // note that docsui treats this as *force* show advanced. So sending
429 // false permits advanced to be shown based on user preferences.
430 intent.putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, isPrimary());
435 public String toString() {
436 final CharArrayWriter writer = new CharArrayWriter();
437 dump(new IndentingPrintWriter(writer, " ", 80));
438 return writer.toString();
441 public void dump(IndentingPrintWriter pw) {
442 pw.println("VolumeInfo{" + id + "}:");
444 pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
445 pw.printPair("diskId", getDiskId());
446 pw.printPair("partGuid", partGuid);
447 pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
448 pw.printPair("mountUserId", mountUserId);
449 pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
451 pw.printPair("fsType", fsType);
452 pw.printPair("fsUuid", fsUuid);
453 pw.printPair("fsLabel", fsLabel);
455 pw.printPair("path", path);
456 pw.printPair("internalPath", internalPath);
462 public VolumeInfo clone() {
463 final Parcel temp = Parcel.obtain();
465 writeToParcel(temp, 0);
466 temp.setDataPosition(0);
467 return CREATOR.createFromParcel(temp);
474 public boolean equals(Object o) {
475 if (o instanceof VolumeInfo) {
476 return Objects.equals(id, ((VolumeInfo) o).id);
483 public int hashCode() {
484 return id.hashCode();
487 public static final Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() {
489 public VolumeInfo createFromParcel(Parcel in) {
490 return new VolumeInfo(in);
494 public VolumeInfo[] newArray(int size) {
495 return new VolumeInfo[size];
500 public int describeContents() {
505 public void writeToParcel(Parcel parcel, int flags) {
506 parcel.writeString(id);
507 parcel.writeInt(type);
510 disk.writeToParcel(parcel, flags);
514 parcel.writeString(partGuid);
515 parcel.writeInt(mountFlags);
516 parcel.writeInt(mountUserId);
517 parcel.writeInt(state);
518 parcel.writeString(fsType);
519 parcel.writeString(fsUuid);
520 parcel.writeString(fsLabel);
521 parcel.writeString(path);
522 parcel.writeString(internalPath);