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