VolumeInfo.java revision 0a38e2318b809dfbd4704705fcf3410d542edfe3
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.Parcel;
27import android.os.Parcelable;
28import android.os.UserHandle;
29import android.provider.DocumentsContract;
30import android.text.TextUtils;
31import android.util.ArrayMap;
32import android.util.DebugUtils;
33import android.util.SparseArray;
34import android.util.SparseIntArray;
35
36import com.android.internal.R;
37import com.android.internal.util.IndentingPrintWriter;
38import com.android.internal.util.Preconditions;
39
40import java.io.CharArrayWriter;
41import java.io.File;
42import java.util.Comparator;
43import 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 */
65public 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        return intent;
447    }
448
449    @Override
450    public String toString() {
451        final CharArrayWriter writer = new CharArrayWriter();
452        dump(new IndentingPrintWriter(writer, "    ", 80));
453        return writer.toString();
454    }
455
456    public void dump(IndentingPrintWriter pw) {
457        pw.println("VolumeInfo{" + id + "}:");
458        pw.increaseIndent();
459        pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
460        pw.printPair("diskId", getDiskId());
461        pw.printPair("partGuid", partGuid);
462        pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
463        pw.printPair("mountUserId", mountUserId);
464        pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
465        pw.println();
466        pw.printPair("fsType", fsType);
467        pw.printPair("fsUuid", fsUuid);
468        pw.printPair("fsLabel", fsLabel);
469        pw.println();
470        pw.printPair("path", path);
471        pw.printPair("internalPath", internalPath);
472        pw.decreaseIndent();
473        pw.println();
474    }
475
476    @Override
477    public VolumeInfo clone() {
478        final Parcel temp = Parcel.obtain();
479        try {
480            writeToParcel(temp, 0);
481            temp.setDataPosition(0);
482            return CREATOR.createFromParcel(temp);
483        } finally {
484            temp.recycle();
485        }
486    }
487
488    @Override
489    public boolean equals(Object o) {
490        if (o instanceof VolumeInfo) {
491            return Objects.equals(id, ((VolumeInfo) o).id);
492        } else {
493            return false;
494        }
495    }
496
497    @Override
498    public int hashCode() {
499        return id.hashCode();
500    }
501
502    public static final Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() {
503        @Override
504        public VolumeInfo createFromParcel(Parcel in) {
505            return new VolumeInfo(in);
506        }
507
508        @Override
509        public VolumeInfo[] newArray(int size) {
510            return new VolumeInfo[size];
511        }
512    };
513
514    @Override
515    public int describeContents() {
516        return 0;
517    }
518
519    @Override
520    public void writeToParcel(Parcel parcel, int flags) {
521        parcel.writeString(id);
522        parcel.writeInt(type);
523        if (disk != null) {
524            parcel.writeInt(1);
525            disk.writeToParcel(parcel, flags);
526        } else {
527            parcel.writeInt(0);
528        }
529        parcel.writeString(partGuid);
530        parcel.writeInt(mountFlags);
531        parcel.writeInt(mountUserId);
532        parcel.writeInt(state);
533        parcel.writeString(fsType);
534        parcel.writeString(fsUuid);
535        parcel.writeString(fsLabel);
536        parcel.writeString(path);
537        parcel.writeString(internalPath);
538    }
539}
540