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