VolumeInfo.java revision b36586a7c9b7718f33961406537e27bbd9b16211
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
133    /** Framework state */
134    public final int mtpIndex;
135
136    public VolumeInfo(String id, int type, DiskInfo disk, int mtpIndex) {
137        this.id = Preconditions.checkNotNull(id);
138        this.type = type;
139        this.disk = disk;
140        this.mtpIndex = mtpIndex;
141    }
142
143    public VolumeInfo(Parcel parcel) {
144        id = parcel.readString();
145        type = parcel.readInt();
146        if (parcel.readInt() != 0) {
147            disk = DiskInfo.CREATOR.createFromParcel(parcel);
148        } else {
149            disk = null;
150        }
151        mountFlags = parcel.readInt();
152        mountUserId = parcel.readInt();
153        state = parcel.readInt();
154        fsType = parcel.readString();
155        fsUuid = parcel.readString();
156        fsLabel = parcel.readString();
157        path = parcel.readString();
158        mtpIndex = parcel.readInt();
159    }
160
161    public static @NonNull String getEnvironmentForState(int state) {
162        final String envState = sStateToEnvironment.get(state);
163        if (envState != null) {
164            return envState;
165        } else {
166            return Environment.MEDIA_UNKNOWN;
167        }
168    }
169
170    public static @Nullable String getBroadcastForEnvironment(String envState) {
171        return sEnvironmentToBroadcast.get(envState);
172    }
173
174    public static @Nullable String getBroadcastForState(int state) {
175        return getBroadcastForEnvironment(getEnvironmentForState(state));
176    }
177
178    public static @NonNull Comparator<VolumeInfo> getDescriptionComparator() {
179        return sDescriptionComparator;
180    }
181
182    public @NonNull String getId() {
183        return id;
184    }
185
186    public @Nullable DiskInfo getDisk() {
187        return disk;
188    }
189
190    public @Nullable String getDiskId() {
191        return (disk != null) ? disk.id : null;
192    }
193
194    public int getType() {
195        return type;
196    }
197
198    public int getState() {
199        return state;
200    }
201
202    public @Nullable String getFsUuid() {
203        return fsUuid;
204    }
205
206    public int getMountUserId() {
207        return mountUserId;
208    }
209
210    public @Nullable String getDescription() {
211        if (ID_PRIVATE_INTERNAL.equals(id)) {
212            return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
213        } else if (!TextUtils.isEmpty(fsLabel)) {
214            return fsLabel;
215        } else {
216            return null;
217        }
218    }
219
220    public boolean isMountedReadable() {
221        return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY;
222    }
223
224    public boolean isMountedWritable() {
225        return state == STATE_MOUNTED;
226    }
227
228    public boolean isPrimary() {
229        return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
230    }
231
232    public boolean isPrimaryPhysical() {
233        return isPrimary() && (getType() == TYPE_PUBLIC);
234    }
235
236    public boolean isVisible() {
237        return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
238    }
239
240    public boolean isVisibleToUser(int userId) {
241        if (type == TYPE_PUBLIC && userId == this.mountUserId) {
242            return isVisible();
243        } else if (type == TYPE_EMULATED) {
244            return isVisible();
245        } else {
246            return false;
247        }
248    }
249
250    public File getPath() {
251        return new File(path);
252    }
253
254    public File getPathForUser(int userId) {
255        if (path == null) {
256            return null;
257        } else if (type == TYPE_PUBLIC && userId == this.mountUserId) {
258            return new File(path);
259        } else if (type == TYPE_EMULATED) {
260            return new File(path, Integer.toString(userId));
261        } else {
262            return null;
263        }
264    }
265
266    /**
267     * Path which is accessible to apps holding
268     * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}.
269     */
270    public File getInternalPathForUser(int userId) {
271        if (type == TYPE_PUBLIC) {
272            // TODO: plumb through cleaner path from vold
273            return new File(path.replace("/storage/", "/mnt/media_rw/"));
274        } else {
275            return getPathForUser(userId);
276        }
277    }
278
279    public StorageVolume buildStorageVolume(Context context, int userId) {
280        final boolean removable;
281        final boolean emulated;
282        final boolean allowMassStorage = false;
283        final int mtpStorageId = MtpStorage.getStorageIdForIndex(mtpIndex);
284        final String envState = getEnvironmentForState(state);
285
286        File userPath = getPathForUser(userId);
287        if (userPath == null) {
288            userPath = new File("/dev/null");
289        }
290
291        String description = getDescription();
292        if (description == null) {
293            description = context.getString(android.R.string.unknownName);
294        }
295
296        long mtpReserveSize = 0;
297        long maxFileSize = 0;
298
299        if (type == TYPE_EMULATED) {
300            emulated = true;
301            mtpReserveSize = StorageManager.from(context).getStorageLowBytes(userPath);
302
303            if (ID_EMULATED_INTERNAL.equals(id)) {
304                removable = false;
305            } else {
306                removable = true;
307            }
308
309        } else if (type == TYPE_PUBLIC) {
310            emulated = false;
311            removable = true;
312
313            if ("vfat".equals(fsType)) {
314                maxFileSize = 4294967295L;
315            }
316
317        } else {
318            throw new IllegalStateException("Unexpected volume type " + type);
319        }
320
321        return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable,
322                emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId),
323                fsUuid, envState);
324    }
325
326    // TODO: avoid this layering violation
327    private static final String DOCUMENT_AUTHORITY = "com.android.externalstorage.documents";
328    private static final String DOCUMENT_ROOT_PRIMARY_EMULATED = "primary";
329
330    /**
331     * Build an intent to browse the contents of this volume. Only valid for
332     * {@link #TYPE_EMULATED} or {@link #TYPE_PUBLIC}.
333     */
334    public Intent buildBrowseIntent() {
335        final Uri uri;
336        if (type == VolumeInfo.TYPE_PUBLIC) {
337            uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY, fsUuid);
338        } else if (VolumeInfo.ID_EMULATED_INTERNAL.equals(id)) {
339            uri = DocumentsContract.buildRootUri(DOCUMENT_AUTHORITY,
340                    DOCUMENT_ROOT_PRIMARY_EMULATED);
341        } else if (type == VolumeInfo.TYPE_EMULATED) {
342            // TODO: build intent once supported
343            uri = null;
344        } else {
345            throw new IllegalArgumentException();
346        }
347
348        final Intent intent = new Intent(DocumentsContract.ACTION_BROWSE_DOCUMENT_ROOT);
349        intent.addCategory(Intent.CATEGORY_DEFAULT);
350        intent.setData(uri);
351        return intent;
352    }
353
354    @Override
355    public String toString() {
356        final CharArrayWriter writer = new CharArrayWriter();
357        dump(new IndentingPrintWriter(writer, "    ", 80));
358        return writer.toString();
359    }
360
361    public void dump(IndentingPrintWriter pw) {
362        pw.println("VolumeInfo{" + id + "}:");
363        pw.increaseIndent();
364        pw.printPair("type", DebugUtils.valueToString(getClass(), "TYPE_", type));
365        pw.printPair("diskId", getDiskId());
366        pw.printPair("mountFlags", DebugUtils.flagsToString(getClass(), "MOUNT_FLAG_", mountFlags));
367        pw.printPair("mountUserId", mountUserId);
368        pw.printPair("state", DebugUtils.valueToString(getClass(), "STATE_", state));
369        pw.println();
370        pw.printPair("fsType", fsType);
371        pw.printPair("fsUuid", fsUuid);
372        pw.printPair("fsLabel", fsLabel);
373        pw.println();
374        pw.printPair("path", path);
375        pw.printPair("mtpIndex", mtpIndex);
376        pw.decreaseIndent();
377        pw.println();
378    }
379
380    @Override
381    public VolumeInfo clone() {
382        final Parcel temp = Parcel.obtain();
383        try {
384            writeToParcel(temp, 0);
385            temp.setDataPosition(0);
386            return CREATOR.createFromParcel(temp);
387        } finally {
388            temp.recycle();
389        }
390    }
391
392    @Override
393    public boolean equals(Object o) {
394        if (o instanceof VolumeInfo) {
395            return Objects.equals(id, ((VolumeInfo) o).id);
396        } else {
397            return false;
398        }
399    }
400
401    @Override
402    public int hashCode() {
403        return id.hashCode();
404    }
405
406    public static final Creator<VolumeInfo> CREATOR = new Creator<VolumeInfo>() {
407        @Override
408        public VolumeInfo createFromParcel(Parcel in) {
409            return new VolumeInfo(in);
410        }
411
412        @Override
413        public VolumeInfo[] newArray(int size) {
414            return new VolumeInfo[size];
415        }
416    };
417
418    @Override
419    public int describeContents() {
420        return 0;
421    }
422
423    @Override
424    public void writeToParcel(Parcel parcel, int flags) {
425        parcel.writeString(id);
426        parcel.writeInt(type);
427        if (disk != null) {
428            parcel.writeInt(1);
429            disk.writeToParcel(parcel, flags);
430        } else {
431            parcel.writeInt(0);
432        }
433        parcel.writeInt(mountFlags);
434        parcel.writeInt(mountUserId);
435        parcel.writeInt(state);
436        parcel.writeString(fsType);
437        parcel.writeString(fsUuid);
438        parcel.writeString(fsLabel);
439        parcel.writeString(path);
440        parcel.writeInt(mtpIndex);
441    }
442}
443