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