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