1 /*
2  * Copyright (C) 2011 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 
17 package android.os.storage;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SystemApi;
22 import android.annotation.TestApi;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.os.Environment;
29 import android.os.Parcel;
30 import android.os.Parcelable;
31 import android.os.UserHandle;
32 import android.provider.DocumentsContract;
33 import android.provider.MediaStore;
34 
35 import com.android.internal.util.IndentingPrintWriter;
36 import com.android.internal.util.Preconditions;
37 
38 import java.io.CharArrayWriter;
39 import java.io.File;
40 import java.util.Locale;
41 
42 /**
43  * Information about a shared/external storage volume for a specific user.
44  *
45  * <p>
46  * A device always has one (and one only) primary storage volume, but it could have extra volumes,
47  * like SD cards and USB drives. This object represents the logical view of a storage
48  * volume for a specific user: different users might have different views for the same physical
49  * volume (for example, if the volume is a built-in emulated storage).
50  *
51  * <p>
52  * The storage volume is not necessarily mounted, applications should use {@link #getState()} to
53  * verify its state.
54  *
55  * <p>
56  * Applications willing to read or write to this storage volume needs to get a permission from the
57  * user first, which can be achieved in the following ways:
58  *
59  * <ul>
60  * <li>To get access to standard directories (like the {@link Environment#DIRECTORY_PICTURES}), they
61  * can use the {@link #createAccessIntent(String)}. This is the recommend way, since it provides a
62  * simpler API and narrows the access to the given directory (and its descendants).
63  * <li>To get access to any directory (and its descendants), they can use the Storage Acess
64  * Framework APIs (such as {@link Intent#ACTION_OPEN_DOCUMENT} and
65  * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, although these APIs do not guarantee the user will
66  * select this specific volume.
67  * <li>To get read and write access to the primary storage volume, applications can declare the
68  * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and
69  * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions respectively, with the
70  * latter including the former. This approach is discouraged, since users may be hesitant to grant
71  * broad access to all files contained on a storage device.
72  * </ul>
73  *
74  * <p>It can be obtained through {@link StorageManager#getStorageVolumes()} and
75  * {@link StorageManager#getPrimaryStorageVolume()} and also as an extra in some broadcasts
76  * (see {@link #EXTRA_STORAGE_VOLUME}).
77  *
78  * <p>
79  * See {@link Environment#getExternalStorageDirectory()} for more info about shared/external
80  * storage semantics.
81  */
82 // NOTE: This is a legacy specialization of VolumeInfo which describes the volume for a specific
83 // user, but is now part of the public API.
84 public final class StorageVolume implements Parcelable {
85 
86     @UnsupportedAppUsage
87     private final String mId;
88     @UnsupportedAppUsage
89     private final File mPath;
90     private final File mInternalPath;
91     @UnsupportedAppUsage
92     private final String mDescription;
93     @UnsupportedAppUsage
94     private final boolean mPrimary;
95     @UnsupportedAppUsage
96     private final boolean mRemovable;
97     private final boolean mEmulated;
98     private final boolean mAllowMassStorage;
99     private final long mMaxFileSize;
100     private final UserHandle mOwner;
101     private final String mFsUuid;
102     private final String mState;
103 
104     /**
105      * Name of the {@link Parcelable} extra in the {@link Intent#ACTION_MEDIA_REMOVED},
106      * {@link Intent#ACTION_MEDIA_UNMOUNTED}, {@link Intent#ACTION_MEDIA_CHECKING},
107      * {@link Intent#ACTION_MEDIA_NOFS}, {@link Intent#ACTION_MEDIA_MOUNTED},
108      * {@link Intent#ACTION_MEDIA_SHARED}, {@link Intent#ACTION_MEDIA_BAD_REMOVAL},
109      * {@link Intent#ACTION_MEDIA_UNMOUNTABLE}, and {@link Intent#ACTION_MEDIA_EJECT} broadcast that
110      * contains a {@link StorageVolume}.
111      */
112     // Also sent on ACTION_MEDIA_UNSHARED, which is @hide
113     public static final String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME";
114 
115     /**
116      * Name of the String extra used by {@link #createAccessIntent(String) createAccessIntent}.
117      *
118      * @hide
119      */
120     public static final String EXTRA_DIRECTORY_NAME = "android.os.storage.extra.DIRECTORY_NAME";
121 
122     /**
123      * Name of the intent used by {@link #createAccessIntent(String) createAccessIntent}.
124      */
125     private static final String ACTION_OPEN_EXTERNAL_DIRECTORY =
126             "android.os.storage.action.OPEN_EXTERNAL_DIRECTORY";
127 
128     /** {@hide} */
129     public static final int STORAGE_ID_INVALID = 0x00000000;
130     /** {@hide} */
131     public static final int STORAGE_ID_PRIMARY = 0x00010001;
132 
133     /** {@hide} */
StorageVolume(String id, File path, File internalPath, String description, boolean primary, boolean removable, boolean emulated, boolean allowMassStorage, long maxFileSize, UserHandle owner, String fsUuid, String state)134     public StorageVolume(String id, File path, File internalPath, String description,
135             boolean primary, boolean removable, boolean emulated, boolean allowMassStorage,
136             long maxFileSize, UserHandle owner, String fsUuid, String state) {
137         mId = Preconditions.checkNotNull(id);
138         mPath = Preconditions.checkNotNull(path);
139         mInternalPath = Preconditions.checkNotNull(internalPath);
140         mDescription = Preconditions.checkNotNull(description);
141         mPrimary = primary;
142         mRemovable = removable;
143         mEmulated = emulated;
144         mAllowMassStorage = allowMassStorage;
145         mMaxFileSize = maxFileSize;
146         mOwner = Preconditions.checkNotNull(owner);
147         mFsUuid = fsUuid;
148         mState = Preconditions.checkNotNull(state);
149     }
150 
StorageVolume(Parcel in)151     private StorageVolume(Parcel in) {
152         mId = in.readString8();
153         mPath = new File(in.readString8());
154         mInternalPath = new File(in.readString8());
155         mDescription = in.readString8();
156         mPrimary = in.readInt() != 0;
157         mRemovable = in.readInt() != 0;
158         mEmulated = in.readInt() != 0;
159         mAllowMassStorage = in.readInt() != 0;
160         mMaxFileSize = in.readLong();
161         mOwner = in.readParcelable(null);
162         mFsUuid = in.readString8();
163         mState = in.readString8();
164     }
165 
166     /**
167      * Return an opaque ID that can be used to identify this volume.
168      *
169      * @hide
170      */
171     @SystemApi
getId()172     public @NonNull String getId() {
173         return mId;
174     }
175 
176     /**
177      * Returns the mount path for the volume.
178      *
179      * @return the mount path
180      * @hide
181      */
182     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}")
183     @TestApi
getPath()184     public String getPath() {
185         return mPath.toString();
186     }
187 
188     /**
189      * Returns the path of the underlying filesystem.
190      *
191      * @return the internal path
192      * @hide
193      */
getInternalPath()194     public String getInternalPath() {
195         return mInternalPath.toString();
196     }
197 
198     /** {@hide} */
199     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}")
getPathFile()200     public File getPathFile() {
201         return mPath;
202     }
203 
204     /**
205      * Returns the directory where this volume is currently mounted.
206      * <p>
207      * Direct filesystem access via this path has significant emulation
208      * overhead, and apps are instead strongly encouraged to interact with media
209      * on storage volumes via the {@link MediaStore} APIs.
210      * <p>
211      * This directory does not give apps any additional access beyond what they
212      * already have via {@link MediaStore}.
213      *
214      * @return directory where this volume is mounted, or {@code null} if the
215      *         volume is not currently mounted.
216      */
getDirectory()217     public @Nullable File getDirectory() {
218         switch (mState) {
219             case Environment.MEDIA_MOUNTED:
220             case Environment.MEDIA_MOUNTED_READ_ONLY:
221                 return mPath;
222             default:
223                 return null;
224         }
225     }
226 
227     /**
228      * Returns a user-visible description of the volume.
229      *
230      * @return the volume description
231      */
getDescription(Context context)232     public String getDescription(Context context) {
233         return mDescription;
234     }
235 
236     /**
237      * Returns true if the volume is the primary shared/external storage, which is the volume
238      * backed by {@link Environment#getExternalStorageDirectory()}.
239      */
isPrimary()240     public boolean isPrimary() {
241         return mPrimary;
242     }
243 
244     /**
245      * Returns true if the volume is removable.
246      *
247      * @return is removable
248      */
isRemovable()249     public boolean isRemovable() {
250         return mRemovable;
251     }
252 
253     /**
254      * Returns true if the volume is emulated.
255      *
256      * @return is removable
257      */
isEmulated()258     public boolean isEmulated() {
259         return mEmulated;
260     }
261 
262     /**
263      * Returns true if this volume can be shared via USB mass storage.
264      *
265      * @return whether mass storage is allowed
266      * @hide
267      */
268     @UnsupportedAppUsage
allowMassStorage()269     public boolean allowMassStorage() {
270         return mAllowMassStorage;
271     }
272 
273     /**
274      * Returns maximum file size for the volume, or zero if it is unbounded.
275      *
276      * @return maximum file size
277      * @hide
278      */
279     @UnsupportedAppUsage
getMaxFileSize()280     public long getMaxFileSize() {
281         return mMaxFileSize;
282     }
283 
284     /** {@hide} */
285     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
getOwner()286     public UserHandle getOwner() {
287         return mOwner;
288     }
289 
290     /**
291      * Gets the volume UUID, if any.
292      */
getUuid()293     public @Nullable String getUuid() {
294         return mFsUuid;
295     }
296 
297     /**
298      * Return the volume name that can be used to interact with this storage
299      * device through {@link MediaStore}.
300      *
301      * @return opaque volume name, or {@code null} if this volume is not indexed
302      *         by {@link MediaStore}.
303      * @see android.provider.MediaStore.Audio.Media#getContentUri(String)
304      * @see android.provider.MediaStore.Video.Media#getContentUri(String)
305      * @see android.provider.MediaStore.Images.Media#getContentUri(String)
306      */
getMediaStoreVolumeName()307     public @Nullable String getMediaStoreVolumeName() {
308         if (isPrimary()) {
309             return MediaStore.VOLUME_EXTERNAL_PRIMARY;
310         } else {
311             return getNormalizedUuid();
312         }
313     }
314 
315     /** {@hide} */
normalizeUuid(@ullable String fsUuid)316     public static @Nullable String normalizeUuid(@Nullable String fsUuid) {
317         return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null;
318     }
319 
320     /** {@hide} */
getNormalizedUuid()321     public @Nullable String getNormalizedUuid() {
322         return normalizeUuid(mFsUuid);
323     }
324 
325     /**
326      * Parse and return volume UUID as FAT volume ID, or return -1 if unable to
327      * parse or UUID is unknown.
328      * @hide
329      */
330     @UnsupportedAppUsage
getFatVolumeId()331     public int getFatVolumeId() {
332         if (mFsUuid == null || mFsUuid.length() != 9) {
333             return -1;
334         }
335         try {
336             return (int) Long.parseLong(mFsUuid.replace("-", ""), 16);
337         } catch (NumberFormatException e) {
338             return -1;
339         }
340     }
341 
342     /** {@hide} */
343     @UnsupportedAppUsage
getUserLabel()344     public String getUserLabel() {
345         return mDescription;
346     }
347 
348     /**
349      * Returns the current state of the volume.
350      *
351      * @return one of {@link Environment#MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED},
352      *         {@link Environment#MEDIA_UNMOUNTED}, {@link Environment#MEDIA_CHECKING},
353      *         {@link Environment#MEDIA_NOFS}, {@link Environment#MEDIA_MOUNTED},
354      *         {@link Environment#MEDIA_MOUNTED_READ_ONLY}, {@link Environment#MEDIA_SHARED},
355      *         {@link Environment#MEDIA_BAD_REMOVAL}, or {@link Environment#MEDIA_UNMOUNTABLE}.
356      */
getState()357     public String getState() {
358         return mState;
359     }
360 
361     /**
362      * Builds an intent to give access to a standard storage directory or entire volume after
363      * obtaining the user's approval.
364      * <p>
365      * When invoked, the system will ask the user to grant access to the requested directory (and
366      * its descendants). The result of the request will be returned to the activity through the
367      * {@code onActivityResult} method.
368      * <p>
369      * To gain access to descendants (child, grandchild, etc) documents, use
370      * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)}, or
371      * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI.
372      * <p>
373      * If your application only needs to store internal data, consider using
374      * {@link Context#getExternalFilesDirs(String) Context.getExternalFilesDirs},
375      * {@link Context#getExternalCacheDirs()}, or {@link Context#getExternalMediaDirs()}, which
376      * require no permissions to read or write.
377      * <p>
378      * Access to the entire volume is only available for non-primary volumes (for the primary
379      * volume, apps can use the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and
380      * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions) and should be used
381      * with caution, since users are more likely to deny access when asked for entire volume access
382      * rather than specific directories.
383      *
384      * @param directoryName must be one of {@link Environment#DIRECTORY_MUSIC},
385      *            {@link Environment#DIRECTORY_PODCASTS}, {@link Environment#DIRECTORY_RINGTONES},
386      *            {@link Environment#DIRECTORY_ALARMS}, {@link Environment#DIRECTORY_NOTIFICATIONS},
387      *            {@link Environment#DIRECTORY_PICTURES}, {@link Environment#DIRECTORY_MOVIES},
388      *            {@link Environment#DIRECTORY_DOWNLOADS}, {@link Environment#DIRECTORY_DCIM}, or
389      *            {@link Environment#DIRECTORY_DOCUMENTS}, or {@code null} to request access to the
390      *            entire volume.
391      * @return intent to request access, or {@code null} if the requested directory is invalid for
392      *         that volume.
393      * @see DocumentsContract
394      * @deprecated Callers should migrate to using {@link Intent#ACTION_OPEN_DOCUMENT_TREE} instead.
395      *             Launching this {@link Intent} on devices running
396      *             {@link android.os.Build.VERSION_CODES#Q} or higher, will immediately finish
397      *             with a result code of {@link android.app.Activity#RESULT_CANCELED}.
398      */
399     @Deprecated
createAccessIntent(String directoryName)400     public @Nullable Intent createAccessIntent(String directoryName) {
401         if ((isPrimary() && directoryName == null) ||
402                 (directoryName != null && !Environment.isStandardDirectory(directoryName))) {
403             return null;
404         }
405         final Intent intent = new Intent(ACTION_OPEN_EXTERNAL_DIRECTORY);
406         intent.putExtra(EXTRA_STORAGE_VOLUME, this);
407         intent.putExtra(EXTRA_DIRECTORY_NAME, directoryName);
408         return intent;
409     }
410 
411     /**
412      * Builds an {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to allow the user to grant access to any
413      * directory subtree (or entire volume) from the {@link android.provider.DocumentsProvider}s
414      * available on the device. The initial location of the document navigation will be the root of
415      * this {@link StorageVolume}.
416      *
417      * Note that the returned {@link Intent} simply suggests that the user picks this {@link
418      * StorageVolume} by default, but the user may select a different location. Callers must respect
419      * the user's chosen location, even if it is different from the originally requested location.
420      *
421      * @return intent to {@link Intent#ACTION_OPEN_DOCUMENT_TREE} initially showing the contents
422      *         of this {@link StorageVolume}
423      * @see Intent#ACTION_OPEN_DOCUMENT_TREE
424      */
createOpenDocumentTreeIntent()425     @NonNull public Intent createOpenDocumentTreeIntent() {
426         final String rootId = isEmulated()
427                 ? DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
428                 : mFsUuid;
429         final Uri rootUri = DocumentsContract.buildRootUri(
430                 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, rootId);
431         final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
432                 .putExtra(DocumentsContract.EXTRA_INITIAL_URI, rootUri)
433                 .putExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, true);
434         return intent;
435     }
436 
437     @Override
equals(Object obj)438     public boolean equals(Object obj) {
439         if (obj instanceof StorageVolume && mPath != null) {
440             StorageVolume volume = (StorageVolume)obj;
441             return (mPath.equals(volume.mPath));
442         }
443         return false;
444     }
445 
446     @Override
hashCode()447     public int hashCode() {
448         return mPath.hashCode();
449     }
450 
451     @Override
toString()452     public String toString() {
453         final StringBuilder buffer = new StringBuilder("StorageVolume: ").append(mDescription);
454         if (mFsUuid != null) {
455             buffer.append(" (").append(mFsUuid).append(")");
456         }
457         return buffer.toString();
458     }
459 
460     /** {@hide} */
461     // TODO: find out where toString() is called internally and replace these calls by dump().
dump()462     public String dump() {
463         final CharArrayWriter writer = new CharArrayWriter();
464         dump(new IndentingPrintWriter(writer, "    ", 80));
465         return writer.toString();
466     }
467 
468     /** {@hide} */
dump(IndentingPrintWriter pw)469     public void dump(IndentingPrintWriter pw) {
470         pw.println("StorageVolume:");
471         pw.increaseIndent();
472         pw.printPair("mId", mId);
473         pw.printPair("mPath", mPath);
474         pw.printPair("mInternalPath", mInternalPath);
475         pw.printPair("mDescription", mDescription);
476         pw.printPair("mPrimary", mPrimary);
477         pw.printPair("mRemovable", mRemovable);
478         pw.printPair("mEmulated", mEmulated);
479         pw.printPair("mAllowMassStorage", mAllowMassStorage);
480         pw.printPair("mMaxFileSize", mMaxFileSize);
481         pw.printPair("mOwner", mOwner);
482         pw.printPair("mFsUuid", mFsUuid);
483         pw.printPair("mState", mState);
484         pw.decreaseIndent();
485     }
486 
487     public static final @android.annotation.NonNull Creator<StorageVolume> CREATOR = new Creator<StorageVolume>() {
488         @Override
489         public StorageVolume createFromParcel(Parcel in) {
490             return new StorageVolume(in);
491         }
492 
493         @Override
494         public StorageVolume[] newArray(int size) {
495             return new StorageVolume[size];
496         }
497     };
498 
499     @Override
describeContents()500     public int describeContents() {
501         return 0;
502     }
503 
504     @Override
writeToParcel(Parcel parcel, int flags)505     public void writeToParcel(Parcel parcel, int flags) {
506         parcel.writeString8(mId);
507         parcel.writeString8(mPath.toString());
508         parcel.writeString8(mInternalPath.toString());
509         parcel.writeString8(mDescription);
510         parcel.writeInt(mPrimary ? 1 : 0);
511         parcel.writeInt(mRemovable ? 1 : 0);
512         parcel.writeInt(mEmulated ? 1 : 0);
513         parcel.writeInt(mAllowMassStorage ? 1 : 0);
514         parcel.writeLong(mMaxFileSize);
515         parcel.writeParcelable(mOwner, flags);
516         parcel.writeString8(mFsUuid);
517         parcel.writeString8(mState);
518     }
519 }
520