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