From 04a5d40cf35fb2c2fca2c1bfd573e5916d804ef6 Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Mon, 8 Feb 2016 16:44:06 -0800 Subject: [PATCH] Initial implementation of StorageManager.getVolumesList(). This change makes StorageManager.getVolumesList(), StorageManager.getPrimaryVolume(), and StorageVolume public and adds a buildAccessIntent() in the latter to automatically generate the ACTION_OPEN_EXTERNAL_DIRECTORY intent, but it doesn't change the ACTION_OPEN_EXTERNAL_DIRECTORY implementation yet (i.e., it still takes an URI with the physical path of the directory, instead of a StorageVolume and a directorny name). BUG: 26742218 Change-Id: I36c59c42b6579e125ec7f03c3af141260875a491 --- api/current.txt | 17 ++- api/removed.txt | 8 + api/system-current.txt | 17 ++- api/system-removed.txt | 8 + api/test-current.txt | 17 ++- api/test-removed.txt | 8 + core/java/android/content/Intent.java | 6 + core/java/android/os/Environment.java | 2 +- .../android/os/storage/StorageManager.java | 11 +- .../android/os/storage/StorageVolume.java | 141 ++++++++++++++++-- .../android/provider/DocumentsContract.java | 4 +- 11 files changed, 220 insertions(+), 19 deletions(-) diff --git a/api/current.txt b/api/current.txt index 7fdd5db96f765..d8b1bc6b8aa4c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -8533,7 +8533,6 @@ package android.content { field public static final java.lang.String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"; field public static final java.lang.String ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT"; field public static final java.lang.String ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE"; - field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY"; field public static final java.lang.String ACTION_PACKAGES_SUSPENDED = "android.intent.action.PACKAGES_SUSPENDED"; field public static final java.lang.String ACTION_PACKAGES_UNSUSPENDED = "android.intent.action.PACKAGES_UNSUSPENDED"; field public static final java.lang.String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED"; @@ -29365,11 +29364,27 @@ package android.os.storage { public class StorageManager { method public java.lang.String getMountedObbPath(java.lang.String); + method public android.os.storage.StorageVolume getPrimaryVolume(); + method public android.os.storage.StorageVolume[] getVolumeList(); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); } + public class StorageVolume implements android.os.Parcelable { + method public android.content.Intent createAccessIntent(java.lang.String); + method public int describeContents(); + method public java.lang.String getDescription(android.content.Context); + method public java.lang.String getState(); + method public java.lang.String getUuid(); + method public boolean isEmulated(); + method public boolean isPrimary(); + method public boolean isRemovable(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final java.lang.String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME"; + } + } package android.preference { diff --git a/api/removed.txt b/api/removed.txt index 0bf659438340d..946f75fa8194d 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -15,6 +15,14 @@ package android.app.admin { } +package android.content { + + public class Intent implements java.lang.Cloneable android.os.Parcelable { + field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY"; + } + +} + package android.content.pm { public class PackageInfo implements android.os.Parcelable { diff --git a/api/system-current.txt b/api/system-current.txt index 266900f917e97..f4901f55eb184 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -8839,7 +8839,6 @@ package android.content { field public static final java.lang.String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"; field public static final java.lang.String ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT"; field public static final java.lang.String ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE"; - field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY"; field public static final java.lang.String ACTION_PACKAGES_SUSPENDED = "android.intent.action.PACKAGES_SUSPENDED"; field public static final java.lang.String ACTION_PACKAGES_UNSUSPENDED = "android.intent.action.PACKAGES_UNSUSPENDED"; field public static final java.lang.String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED"; @@ -31713,11 +31712,27 @@ package android.os.storage { public class StorageManager { method public java.lang.String getMountedObbPath(java.lang.String); + method public android.os.storage.StorageVolume getPrimaryVolume(); + method public android.os.storage.StorageVolume[] getVolumeList(); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); } + public class StorageVolume implements android.os.Parcelable { + method public android.content.Intent createAccessIntent(java.lang.String); + method public int describeContents(); + method public java.lang.String getDescription(android.content.Context); + method public java.lang.String getState(); + method public java.lang.String getUuid(); + method public boolean isEmulated(); + method public boolean isPrimary(); + method public boolean isRemovable(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final java.lang.String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME"; + } + } package android.preference { diff --git a/api/system-removed.txt b/api/system-removed.txt index 27de91312fae7..ca3c85ab21576 100644 --- a/api/system-removed.txt +++ b/api/system-removed.txt @@ -6,6 +6,14 @@ package android.app { } +package android.content { + + public class Intent implements java.lang.Cloneable android.os.Parcelable { + field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY"; + } + +} + package android.content.pm { public class PackageInfo implements android.os.Parcelable { diff --git a/api/test-current.txt b/api/test-current.txt index e940378f12ca0..ab3ced6e478b5 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -8538,7 +8538,6 @@ package android.content { field public static final java.lang.String ACTION_NEW_OUTGOING_CALL = "android.intent.action.NEW_OUTGOING_CALL"; field public static final java.lang.String ACTION_OPEN_DOCUMENT = "android.intent.action.OPEN_DOCUMENT"; field public static final java.lang.String ACTION_OPEN_DOCUMENT_TREE = "android.intent.action.OPEN_DOCUMENT_TREE"; - field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY"; field public static final java.lang.String ACTION_PACKAGES_SUSPENDED = "android.intent.action.PACKAGES_SUSPENDED"; field public static final java.lang.String ACTION_PACKAGES_UNSUSPENDED = "android.intent.action.PACKAGES_UNSUSPENDED"; field public static final java.lang.String ACTION_PACKAGE_ADDED = "android.intent.action.PACKAGE_ADDED"; @@ -29375,11 +29374,27 @@ package android.os.storage { public class StorageManager { method public java.lang.String getMountedObbPath(java.lang.String); + method public android.os.storage.StorageVolume getPrimaryVolume(); + method public android.os.storage.StorageVolume[] getVolumeList(); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); } + public class StorageVolume implements android.os.Parcelable { + method public android.content.Intent createAccessIntent(java.lang.String); + method public int describeContents(); + method public java.lang.String getDescription(android.content.Context); + method public java.lang.String getState(); + method public java.lang.String getUuid(); + method public boolean isEmulated(); + method public boolean isPrimary(); + method public boolean isRemovable(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final java.lang.String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME"; + } + } package android.preference { diff --git a/api/test-removed.txt b/api/test-removed.txt index 0bf659438340d..946f75fa8194d 100644 --- a/api/test-removed.txt +++ b/api/test-removed.txt @@ -15,6 +15,14 @@ package android.app.admin { } +package android.content { + + public class Intent implements java.lang.Cloneable android.os.Parcelable { + field public static final java.lang.String ACTION_OPEN_EXTERNAL_DIRECTORY = "android.intent.action.OPEN_EXTERNAL_DIRECTORY"; + } + +} + package android.content.pm { public class PackageInfo implements android.os.Parcelable { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index b476a25515f5e..4e78b8ad693c1 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -3213,6 +3213,12 @@ public class Intent implements Parcelable, Cloneable { * Output: The URI representing the requested directory tree. * * @see DocumentsContract + * + * {@removed} + * + * Will be removed / hidden before N is published; apps should use + * {@link android.os.storage.StorageManager#getVolumeList()} and + * {@link android.os.storage.StorageVolume#createAccessIntent(String)} instead. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 59bf2938cfcec..edf2da2d9142d 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -339,7 +339,7 @@ public class Environment { *

* Writing to this path requires the * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission, - * and starting in read access requires the + * and starting in {@link android.os.Build.VERSION_CODES#KITKAT}, read access requires the * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} permission, * which is automatically granted if you hold the write permission. *

diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index e7dfbd72292ae..97ee90dc9ea23 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -865,7 +865,12 @@ public class StorageManager { } } - /** {@hide} */ + /** + * Gets the list of shared/external storage volumes available to the current user. + * + *

It always contains the primary storage volume, plus any additional external volume(s) + * available in the device, such as SD cards or attached USB drives. + */ public @NonNull StorageVolume[] getVolumeList() { return getVolumeList(mContext.getUserId(), 0); } @@ -914,7 +919,9 @@ public class StorageManager { return paths; } - /** {@hide} */ + /** + * Gets the primary shared/external storage volume available to the current user. + */ public @NonNull StorageVolume getPrimaryVolume() { return getPrimaryVolume(getVolumeList()); } diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index 1408202a35ea4..60e12665857c7 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -16,11 +16,17 @@ package android.os.storage; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.content.Intent; import android.net.TrafficStats; +import android.net.Uri; +import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; +import android.provider.DocumentsContract; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; @@ -29,14 +35,46 @@ import java.io.CharArrayWriter; import java.io.File; /** - * Information about a storage volume that may be mounted. This is a legacy - * specialization of {@link VolumeInfo} which describes the volume for a - * specific user. - *

- * This class may be deprecated in the future. + * Information about a shared/external storage volume for a specific user. * - * @hide + *

+ * A device always has one (and one only) primary storage volume, but it could have extra volumes, + * like SD cards and USB drives. This object represents the logical view of a storage + * volume for a specific user: different users might have different views for the same physical + * volume (for example, if the volume is a built-in emulated storage). + * + *

+ * The storage volume is not necessarily mounted, applications should use {@link #getState()} to + * verify its state. + * + *

+ * Applications willing to read or write to this storage volume needs to get a permission from the + * user first, which can be achieved in the following ways: + * + *

    + *
  • To get access to standard directories (like the {@link Environment#DIRECTORY_PICTURES}), they + * can use the {@link #createAccessIntent(String)}. This is the recommend way, since it provides a + * simpler API and narrows the access to the given directory (and its descendants). + *
  • To get access to any directory (and its descendants), they can use the Storage Area Framework + * APIs (such as {@link Intent#ACTION_OPEN_DOCUMENT} and {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, + * although these APIs do not guarantee the user will select this specific volume. + *
  • To get read and write access to the primary storage volume, applications can declare the + * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions respectively, with the + * latter including the former. This approach is discouraged, since users may be hesitant to grant + * broad access to all files contained on a storage device. + *
+ * + *

It can be obtained through {@link StorageManager#getVolumeList()} and + * {@link StorageManager#getPrimaryVolume()} and also as an extra in some broadcasts + * (see {@link #EXTRA_STORAGE_VOLUME}). + * + *

+ * See {@link Environment#getExternalStorageDirectory()} for more info about shared/external + * storage semantics. */ +// NOTE: This is a legacy specialization of VolumeInfo which describes the volume for a specific +// user, but is now part of the public API. public class StorageVolume implements Parcelable { private final String mId; @@ -53,14 +91,23 @@ public class StorageVolume implements Parcelable { private final String mFsUuid; private final String mState; - // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING, - // ACTION_MEDIA_NOFS, ACTION_MEDIA_MOUNTED, ACTION_MEDIA_SHARED, ACTION_MEDIA_UNSHARED, - // ACTION_MEDIA_BAD_REMOVAL, ACTION_MEDIA_UNMOUNTABLE and ACTION_MEDIA_EJECT broadcasts. - public static final String EXTRA_STORAGE_VOLUME = "storage_volume"; + /** + * Name of the {@link Parcelable} extra in the {@link Intent#ACTION_MEDIA_REMOVED}, + * {@link Intent#ACTION_MEDIA_UNMOUNTED}, {@link Intent#ACTION_MEDIA_CHECKING}, + * {@link Intent#ACTION_MEDIA_NOFS}, {@link Intent#ACTION_MEDIA_MOUNTED}, + * {@link Intent#ACTION_MEDIA_SHARED}, {@link Intent#ACTION_MEDIA_BAD_REMOVAL}, + * {@link Intent#ACTION_MEDIA_UNMOUNTABLE}, and {@link Intent#ACTION_MEDIA_EJECT} broadcast that + * contains a {@link StorageVolume}. + */ + // Also sent on ACTION_MEDIA_UNSHARED, which is @hide + public static final String EXTRA_STORAGE_VOLUME = "android.os.storage.extra.STORAGE_VOLUME"; + /** {@hide} */ public static final int STORAGE_ID_INVALID = 0x00000000; + /** {@hide} */ public static final int STORAGE_ID_PRIMARY = 0x00010001; + /** {@hide} */ public StorageVolume(String id, int storageId, File path, String description, boolean primary, boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage, long maxFileSize, UserHandle owner, String fsUuid, String state) { @@ -95,6 +142,7 @@ public class StorageVolume implements Parcelable { mState = in.readString(); } + /** {@hide} */ public String getId() { return mId; } @@ -103,17 +151,19 @@ public class StorageVolume implements Parcelable { * Returns the mount path for the volume. * * @return the mount path + * @hide */ public String getPath() { return mPath.toString(); } + /** {@hide} */ public File getPathFile() { return mPath; } /** - * Returns a user visible description of the volume. + * Returns a user-visible description of the volume. * * @return the volume description */ @@ -121,6 +171,10 @@ public class StorageVolume implements Parcelable { return mDescription; } + /** + * Returns true if the volume is the primary shared/external storage, which is the volume + * backed by {@link Environment#getExternalStorageDirectory()}. + */ public boolean isPrimary() { return mPrimary; } @@ -148,6 +202,7 @@ public class StorageVolume implements Parcelable { * this is also used for the storage_id column in the media provider. * * @return MTP storage ID + * @hide */ public int getStorageId() { return mStorageId; @@ -164,6 +219,7 @@ public class StorageVolume implements Parcelable { * too close to full. * * @return MTP reserve space + * @hide */ public int getMtpReserveSpace() { return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES); @@ -173,6 +229,7 @@ public class StorageVolume implements Parcelable { * Returns true if this volume can be shared via USB mass storage. * * @return whether mass storage is allowed + * @hide */ public boolean allowMassStorage() { return mAllowMassStorage; @@ -182,22 +239,28 @@ public class StorageVolume implements Parcelable { * Returns maximum file size for the volume, or zero if it is unbounded. * * @return maximum file size + * @hide */ public long getMaxFileSize() { return mMaxFileSize; } + /** {@hide} */ public UserHandle getOwner() { return mOwner; } - public String getUuid() { + /** + * Gets the volume UUID, if any. + */ + public @Nullable String getUuid() { return mFsUuid; } /** * Parse and return volume UUID as FAT volume ID, or return -1 if unable to * parse or UUID is unknown. + * @hide */ public int getFatVolumeId() { if (mFsUuid == null || mFsUuid.length() != 9) { @@ -210,14 +273,56 @@ public class StorageVolume implements Parcelable { } } + /** {@hide} */ public String getUserLabel() { return mDescription; } + /** + * Returns the current state of the volume. + * + * @return one of {@link Environment#MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED}, + * {@link Environment#MEDIA_UNMOUNTED}, {@link Environment#MEDIA_CHECKING}, + * {@link Environment#MEDIA_NOFS}, {@link Environment#MEDIA_MOUNTED}, + * {@link Environment#MEDIA_MOUNTED_READ_ONLY}, {@link Environment#MEDIA_SHARED}, + * {@link Environment#MEDIA_BAD_REMOVAL}, or {@link Environment#MEDIA_UNMOUNTABLE}. + */ public String getState() { return mState; } + /** + * Builds an intent to give access to a standard storage directory after obtaining the user's + * approval. + *

+ * When invoked, the system will ask the user to grant access to the requested directory (and + * its descendants). The result of the request will be returned to the activity through the + * {@code onActivityResult} method. + *

+ * To gain access to descendants (child, grandchild, etc) documents, use + * {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)}, or + * {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI. + * + * If your application only needs to store internal data, consider using + * {@link Context#getExternalFilesDirs(String) Context.getExternalFilesDirs}, + * {@link Context#getExternalCacheDirs()}, or + * {@link Context#getExternalMediaDirs()}, which require no permissions to read or write. + * + * @param directoryName must be one of + * {@link Environment#DIRECTORY_MUSIC}, {@link Environment#DIRECTORY_PODCASTS}, + * {@link Environment#DIRECTORY_RINGTONES}, {@link Environment#DIRECTORY_ALARMS}, + * {@link Environment#DIRECTORY_NOTIFICATIONS}, {@link Environment#DIRECTORY_PICTURES}, + * {@link Environment#DIRECTORY_MOVIES}, {@link Environment#DIRECTORY_DOWNLOADS}, + * {@link Environment#DIRECTORY_DCIM}, or {@link Environment#DIRECTORY_DOCUMENTS} + * + * @see DocumentsContract + */ + public Intent createAccessIntent(@NonNull String directoryName) { + final Intent intent = new Intent(Intent.ACTION_OPEN_EXTERNAL_DIRECTORY); + intent.setData(Uri.fromFile(new File(mPath, directoryName))); + return intent; + } + @Override public boolean equals(Object obj) { if (obj instanceof StorageVolume && mPath != null) { @@ -234,11 +339,23 @@ public class StorageVolume implements Parcelable { @Override public String toString() { + final StringBuilder buffer = new StringBuilder("StorageVolume: ").append(mDescription); + if (mFsUuid != null) { + buffer.append(" (").append(mFsUuid).append(")"); + } + return buffer.toString(); + } + + /** {@hide} */ + // TODO(b/26742218): find out where toString() is called internally and replace these calls by + // dump(). + public String dump() { final CharArrayWriter writer = new CharArrayWriter(); dump(new IndentingPrintWriter(writer, " ", 80)); return writer.toString(); } + /** {@hide} */ public void dump(IndentingPrintWriter pw) { pw.println("StorageVolume:"); pw.increaseIndent(); diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 2ca758935feb5..9cfab836110c5 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -39,6 +39,7 @@ import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.os.RemoteException; +import android.os.storage.StorageVolume; import android.system.ErrnoException; import android.system.Os; import android.util.Log; @@ -62,7 +63,8 @@ import java.util.List; * All client apps must hold a valid URI permission grant to access documents, * typically issued when a user makes a selection through * {@link Intent#ACTION_OPEN_DOCUMENT}, {@link Intent#ACTION_CREATE_DOCUMENT}, - * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, or {@link Intent#ACTION_OPEN_EXTERNAL_DIRECTORY}. + * {@link Intent#ACTION_OPEN_DOCUMENT_TREE}, or + * {@link StorageVolume#createAccessIntent(String) StorageVolume.createAccessIntent}. * * @see DocumentsProvider */