VolumeInfo.java revision 0000d8aef85058d290527efd452bd3ac7bc09804
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
17package android.os.storage;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.net.Uri;
25import android.os.Environment;
26import android.os.IVold;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.os.UserHandle;
30import android.provider.DocumentsContract;
31import android.text.TextUtils;
32import android.util.ArrayMap;
33import android.util.DebugUtils;
34import android.util.SparseArray;
35import android.util.SparseIntArray;
36
37import com.android.internal.R;
38import com.android.internal.util.IndentingPrintWriter;
39import com.android.internal.util.Preconditions;
40
41import java.io.CharArrayWriter;
42import java.io.File;
43import java.util.Comparator;
44import java.util.Objects;
45
46/**
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.
50 * <p>
51 * Volumes may be mounted with various flags:
52 * <ul>
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.
62 * </ul>
63 *
64 * @hide
65 */
66public 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";
73
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";
78
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;
84
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;
94
95    public static final int MOUNT_FLAG_PRIMARY = IVold.MOUNT_FLAG_PRIMARY;
96    public static final int MOUNT_FLAG_VISIBLE = IVold.MOUNT_FLAG_VISIBLE;
97
98    private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
99    private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
100    private static SparseIntArray sStateToDescrip = new SparseIntArray();
101
102    private static final Comparator<VolumeInfo>
103            sDescriptionComparator = new Comparator<VolumeInfo>() {
104        @Override
105        public int compare(VolumeInfo lhs, VolumeInfo rhs) {
106            if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(lhs.getId())) {
107                return -1;
108            } else if (lhs.getDescription() == null) {
109                return 1;
110            } else if (rhs.getDescription() == null) {
111                return -1;
112            } else {
113                return lhs.getDescription().compareTo(rhs.getDescription());
114            }
115        }
116    };
117
118    static {
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);
128
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);
137
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);
147    }
148
149    /** vold state */
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;
160    public String path;
161    public String internalPath;
162
163    public VolumeInfo(String id, int type, DiskInfo disk, String partGuid) {
164        this.id = Preconditions.checkNotNull(id);
165        this.type = type;
166        this.disk = disk;
167        this.partGuid = partGuid;
168    }
169
170    public VolumeInfo(Parcel parcel) {
171        id = parcel.readString();
172        type = parcel.readInt();
173        if (parcel.readInt() != 0) {
174            disk = DiskInfo.CREATOR.createFromParcel(parcel);
175        } else {
176            disk = null;
177        }
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();
187    }
188
189    public static @NonNull String getEnvironmentForState(int state) {
190        final String envState = sStateToEnvironment.get(state);
191        if (envState != null) {
192            return envState;
193        } else {
194            return Environment.MEDIA_UNKNOWN;
195        }
196    }
197
198    public static @Nullable String getBroadcastForEnvironment(String envState) {
199        return sEnvironmentToBroadcast.get(envState);
200    }
201
202    public static @Nullable String getBroadcastForState(int state) {
203        return getBroadcastForEnvironment(getEnvironmentForState(state));
204    }
205
206    public static @NonNull Comparator<VolumeInfo> getDescriptionComparator() {
207        return sDescriptionComparator;
208    }
209
210    public @NonNull String getId() {
211        return id;
212    }
213
214    public @Nullable DiskInfo getDisk() {
215        return disk;
216    }
217
218    public @Nullable String getDiskId() {
219        return (disk != null) ? disk.id : null;
220    }
221
222    public int getType() {
223        return type;
224    }
225
226    public int getState() {
227        return state;
228    }
229
230    public int getStateDescription() {
231        return sStateToDescrip.get(state, 0);
232    }
233
234    public @Nullable String getFsUuid() {
235        return fsUuid;
236    }
237
238    public int getMountUserId() {
239        return mountUserId;
240    }
241
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)) {
246            return fsLabel;
247        } else {
248            return null;
249        }
250    }
251
252    public boolean isMountedReadable() {
253        return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY;
254    }
255
256    public boolean isMountedWritable() {
257        return state == STATE_MOUNTED;
258    }
259
260    public boolean isPrimary() {
261        return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
262    }
263
264    public boolean isPrimaryPhysical() {
265        return isPrimary() && (getType() == TYPE_PUBLIC);
266    }
267
268    public boolean isVisible() {
269        return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
270    }
271
272    public boolean isVisibleForUser(int userId) {
273        if (type == TYPE_PUBLIC && mountUserId == userId) {
274            return isVisible();
275        } else if (type == TYPE_EMULATED) {
276            return isVisible();
277        } else {
278            return false;
279        }
280    }
281
282    public boolean isVisibleForRead(int userId) {
283        return isVisibleForUser(userId);
284    }
285
286    public boolean isVisibleForWrite(int userId) {
287        return isVisibleForUser(userId);
288    }
289
290    public File getPath() {
291        return (path != null) ? new File(path) : null;
292    }
293
294    public File getInternalPath() {
295        return (internalPath != null) ? new File(internalPath) : null;
296    }
297
298    public File getPathForUser(int userId) {
299        if (path == null) {
300            return null;
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));
305        } else {
306            return null;
307        }
308    }
309
310    /**
311     * Path which is accessible to apps holding
312     * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}.
313     */
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/"));
318        } else {
319            return getPathForUser(userId);
320        }
321    }
322
323    public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
324        final StorageManager storage = context.getSystemService(StorageManager.class);
325
326        final boolean removable;
327        final boolean emulated;
328        final boolean allowMassStorage = false;
329        final String envState = reportUnmounted
330                ? Environment.MEDIA_UNMOUNTED : getEnvironmentForState(state);
331
332        File userPath = getPathForUser(userId);
333        if (userPath == null) {
334            userPath = new File("/dev/null");
335        }
336
337        String description = null;
338        String derivedFsUuid = fsUuid;
339        long maxFileSize = 0;
340
341        if (type == TYPE_EMULATED) {
342            emulated = true;
343
344            final VolumeInfo privateVol = storage.findPrivateForEmulated(this);
345            if (privateVol != null) {
346                description = storage.getBestVolumeDescription(privateVol);
347                derivedFsUuid = privateVol.fsUuid;
348            }
349
350            if (ID_EMULATED_INTERNAL.equals(id)) {
351                removable = false;
352            } else {
353                removable = true;
354            }
355
356        } else if (type == TYPE_PUBLIC) {
357            emulated = false;
358            removable = true;
359
360            description = storage.getBestVolumeDescription(this);
361
362            if ("vfat".equals(fsType)) {
363                maxFileSize = 4294967295L;
364            }
365
366        } else {
367            throw new IllegalStateException("Unexpected volume type " + type);
368        }
369
370        if (description == null) {
371            description = context.getString(android.R.string.unknownName);
372        }
373
374        return new StorageVolume(id, userPath, description, isPrimary(), removable,
375                emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
376                derivedFsUuid, envState);
377    }
378
379    public static int buildStableMtpStorageId(String fsUuid) {
380        if (TextUtils.isEmpty(fsUuid)) {
381            return StorageVolume.STORAGE_ID_INVALID;
382        } else {
383            int hash = 0;
384            for (int i = 0; i < fsUuid.length(); ++i) {
385                hash = 31 * hash + fsUuid.charAt(i);
386            }
387            hash = (hash ^ (hash << 16)) & 0xffff0000;
388            // Work around values that the spec doesn't allow, or that we've
389            // reserved for primary
390            if (hash == 0x00000000) hash = 0x00020000;
391            if (hash == 0x00010000) hash = 0x00020000;
392            if (hash == 0xffff0000) hash = 0xfffe0000;
393            return hash | 0x0001;
394        }
395    }
396
397    // TODO: avoid this layering violation
398    private static final String DOCUMENT_AUTHORITY = "com.android.externalstorage.documents";
399    private static final String DOCUMENT_ROOT_PRIMARY_EMULATED = "primary";
400
401    /**
402     * Build an intent to browse the contents of this volume. Only valid for
403     * {@link #TYPE_EMULATED} or {@link #TYPE_PUBLIC}.
404     */
405    public @Nullable Intent buildBrowseIntent() {
406        final Uri uri;
407        if (type == VolumeInfo.TYPE_PUBLIC && mountUserId == UserHandle.myUserId()) {
408            uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, fsUuid);
409        } else if (type == VolumeInfo.TYPE_EMULATED && isPrimary()) {
410            uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY,
411                    DOCUMENT_ROOT_PRIMARY_EMULATED);
412        } else {
413            return null;
414        }
415
416        final Intent intent = new Intent(Intent.ACTION_VIEW);
417        intent.addCategory(Intent.CATEGORY_DEFAULT);
418        intent.setDataAndType(uri, DocumentsContract.Root.MIME_TYPE_ITEM);
419
420        // note that docsui treats this as *force* show advanced. So sending
421        // false permits advanced to be shown based on user preferences.
422        intent.putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, isPrimary());
423        return intent;
424    }
425
426    @Override
427    public String toString() {
428        final CharArrayWriter writer = new CharArrayWriter();
429        dump(new IndentingPrintWriter(writer, "    ", 80));
430        return writer.toString();
431    }
432
433    public void dump(IndentingPrintWriter pw) {
434        pw.println("VolumeInfo{" + id + "}:");
435        pw.increaseIndent();
436        pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
437        pw.printPair("diskId", getDiskId());
438        pw.printPair("partGuid", partGuid);
439        pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
440        pw.printPair("mountUserId", mountUserId);
441        pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
442        pw.println();
443        pw.printPair("fsType", fsType);
444        pw.printPair("fsUuid", fsUuid);
445        pw.printPair("fsLabel", fsLabel);
446        pw.println();
447        pw.printPair("path", path);
448        pw.printPair("internalPath", internalPath);
449        pw.decreaseIndent();
450        pw.println();
451    }
452
453    @Override
454    public VolumeInfo clone() {
455        final Parcel temp = Parcel.obtain();
456        try {
457            writeToParcel(temp, 0);
458            temp.setDataPosition(0);
459            return CREATOR.createFromParcel(temp);
460        } finally {
461            temp.recycle();
462        }
463    }
464
465    @Override
466    public boolean equals(Object o) {
467        if (o instanceof VolumeInfo) {
468            return Objects.equals(id, ((VolumeInfo) o).id);
469        } else {
470            return false;
471        }
472    }
473
474    @Override
475    public int hashCode() {
476        return id.hashCode();
477    }
478
479    public static final Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() {
480        @Override
481        public VolumeInfo createFromParcel(Parcel in) {
482            return new VolumeInfo(in);
483        }
484
485        @Override
486        public VolumeInfo[] newArray(int size) {
487            return new VolumeInfo[size];
488        }
489    };
490
491    @Override
492    public int describeContents() {
493        return 0;
494    }
495
496    @Override
497    public void writeToParcel(Parcel parcel, int flags) {
498        parcel.writeString(id);
499        parcel.writeInt(type);
500        if (disk != null) {
501            parcel.writeInt(1);
502            disk.writeToParcel(parcel, flags);
503        } else {
504            parcel.writeInt(0);
505        }
506        parcel.writeString(partGuid);
507        parcel.writeInt(mountFlags);
508        parcel.writeInt(mountUserId);
509        parcel.writeInt(state);
510        parcel.writeString(fsType);
511        parcel.writeString(fsUuid);
512        parcel.writeString(fsLabel);
513        parcel.writeString(path);
514        parcel.writeString(internalPath);
515    }
516}
517