diff options
| author | 2019-04-09 23:46:48 -0600 | |
|---|---|---|
| committer | 2019-04-10 09:35:14 -0600 | |
| commit | 586d3c013a168e36cebb100bd1afa7f04446819f (patch) | |
| tree | f1ad3fe905b0ca7dc279f5e7dfebfc7ec14dd898 | |
| parent | 10f383b32c204e21b849aaf51ebc3ae0828b2a40 (diff) | |
Split VOLUME_EXTERNAL and VOLUME_PRIMARY.
To let developers focus on specific concrete storage devices in Q,
we need a volume name that can be used to point at the primary
external storage device. We had been using VOLUME_EXTERNAL for that,
but we've heard that certain apps are making deep assumptions that
media item IDs are globally unique across all volumes.
Thus these changes merge all volumes back into a single underlying
database, and VOLUME_EXTERNAL works with all of the currently
attached volumes. The new VOLUME_PRIMARY name can be used to focus
on the primary storage device when desired.
When developers try inserting items directly into VOLUME_EXTERNAL,
we gracefully assume they meant VOLUME_PRIMARY.
Bug: 128451765
Test: atest --test-mapping packages/providers/MediaProvider
Change-Id: I682ff6e9aaab4f5315a46c9825313a438548c7e6
| -rw-r--r-- | api/current.txt | 4 | ||||
| -rw-r--r-- | api/removed.txt | 1 | ||||
| -rw-r--r-- | core/java/android/os/storage/StorageManager.java | 2 | ||||
| -rw-r--r-- | core/java/android/os/storage/StorageVolume.java | 7 | ||||
| -rw-r--r-- | core/java/android/provider/MediaStore.java | 122 |
5 files changed, 98 insertions, 38 deletions
diff --git a/api/current.txt b/api/current.txt index b6249467fc62..b5d44b7f9573 100644 --- a/api/current.txt +++ b/api/current.txt @@ -38478,8 +38478,8 @@ package android.provider { public final class MediaStore { ctor public MediaStore(); - method @NonNull public static java.util.Set<java.lang.String> getAllVolumeNames(@NonNull android.content.Context); method @Nullable public static android.net.Uri getDocumentUri(@NonNull android.content.Context, @NonNull android.net.Uri); + method @NonNull public static java.util.Set<java.lang.String> getExternalVolumeNames(@NonNull android.content.Context); method public static android.net.Uri getMediaScannerUri(); method @Nullable public static android.net.Uri getMediaUri(@NonNull android.content.Context, @NonNull android.net.Uri); method @NonNull public static String getVersion(@NonNull android.content.Context); @@ -38523,6 +38523,7 @@ package android.provider { field public static final String META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE = "android.media.still_image_camera_preview_service"; field public static final String UNKNOWN_STRING = "<unknown>"; field public static final String VOLUME_EXTERNAL = "external"; + field public static final String VOLUME_EXTERNAL_PRIMARY = "external_primary"; field public static final String VOLUME_INTERNAL = "internal"; } @@ -38771,6 +38772,7 @@ package android.provider { field public static final String RELATIVE_PATH = "relative_path"; field public static final String SIZE = "_size"; field public static final String TITLE = "title"; + field public static final String VOLUME_NAME = "volume_name"; field public static final String WIDTH = "width"; } diff --git a/api/removed.txt b/api/removed.txt index fe3e866de682..70ff50ed40a6 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -514,6 +514,7 @@ package android.provider { public final class MediaStore { method @Deprecated @NonNull public static android.net.Uri createPending(@NonNull android.content.Context, @NonNull android.provider.MediaStore.PendingParams); + method @Deprecated @NonNull public static java.util.Set<java.lang.String> getAllVolumeNames(@NonNull android.content.Context); method @Deprecated @NonNull public static android.provider.MediaStore.PendingSession openPending(@NonNull android.content.Context, @NonNull android.net.Uri); method @Deprecated @NonNull public static android.net.Uri setIncludeTrashed(@NonNull android.net.Uri); method @Deprecated public static void trash(@NonNull android.content.Context, @NonNull android.net.Uri); diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 075b650ed8f4..080ff7301ed2 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1130,7 +1130,7 @@ public class StorageManager { public @NonNull StorageVolume getStorageVolume(@NonNull Uri uri) { final String volumeName = MediaStore.getVolumeName(uri); switch (volumeName) { - case MediaStore.VOLUME_EXTERNAL: + case MediaStore.VOLUME_EXTERNAL_PRIMARY: return getPrimaryStorageVolume(); default: for (StorageVolume vol : getStorageVolumes()) { diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index 225ecfa315aa..6280600823d7 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -265,8 +265,13 @@ public final class StorageVolume implements Parcelable { } /** {@hide} */ + public static @Nullable String normalizeUuid(@Nullable String fsUuid) { + return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null; + } + + /** {@hide} */ public @Nullable String getNormalizedUuid() { - return mFsUuid != null ? mFsUuid.toLowerCase(Locale.US) : null; + return normalizeUuid(mFsUuid); } /** diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 0b1647d05ef4..da19d59367a0 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -102,20 +102,40 @@ public final class MediaStore { public static final @NonNull Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); /** - * Volume name used for content on "internal" storage of device. This - * volume contains media distributed with the device, such as built-in - * ringtones and wallpapers. + * Synthetic volume name that provides a view of all content across the + * "internal" storage of the device. + * <p> + * This synthetic volume provides a merged view of all media distributed + * with the device, such as built-in ringtones and wallpapers. + * <p> + * Because this is a synthetic volume, you can't insert new content into + * this volume. */ public static final String VOLUME_INTERNAL = "internal"; /** - * Volume name used for content on "external" storage of device. This only - * includes media on the primary shared storage device; the contents of any - * secondary storage devices can be obtained using - * {@link #getAllVolumeNames(Context)}. + * Synthetic volume name that provides a view of all content across the + * "external" storage of the device. + * <p> + * This synthetic volume provides a merged view of all media across all + * currently attached external storage devices. + * <p> + * Because this is a synthetic volume, you can't insert new content into + * this volume. Instead, you can insert content into a specific storage + * volume obtained from {@link #getExternalVolumeNames(Context)}. */ public static final String VOLUME_EXTERNAL = "external"; + /** + * Specific volume name that represents the primary external storage device + * at {@link Environment#getExternalStorageDirectory()}. + * <p> + * This volume may not always be available, such as when the user has + * ejected the device. You can find a list of all specific volume names + * using {@link #getExternalVolumeNames(Context)}. + */ + public static final String VOLUME_EXTERNAL_PRIMARY = "external_primary"; + /** {@hide} */ public static final String SCAN_FILE_CALL = "scan_file"; /** {@hide} */ @@ -1037,6 +1057,16 @@ public final class MediaStore { public static final String OWNER_PACKAGE_NAME = "owner_package_name"; /** + * Volume name of the specific storage device where this media item is + * persisted. The value is typically one of the volume names returned + * from {@link MediaStore#getExternalVolumeNames(Context)}. + * <p> + * This is a read-only column that is automatically computed. + */ + @Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true) + public static final String VOLUME_NAME = "volume_name"; + + /** * Relative path of this media item within the storage device where it * is persisted. For example, an item stored at * {@code /storage/0000-0000/DCIM/Vacation/IMG1024.JPG} would have a @@ -1408,7 +1438,7 @@ public final class MediaStore { final StorageVolume sv = sm.getStorageVolume(path); if (sv != null) { if (sv.isPrimary()) { - return VOLUME_EXTERNAL; + return VOLUME_EXTERNAL_PRIMARY; } else { return checkArgumentVolumeName(sv.getNormalizedUuid()); } @@ -1710,7 +1740,7 @@ public final class MediaStore { String stringUrl = null; /* value to be returned */ try { - url = cr.insert(EXTERNAL_CONTENT_URI, values); + url = cr.insert(getContentUri(VOLUME_EXTERNAL_PRIMARY), values); if (source != null) { try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream( @@ -3224,22 +3254,29 @@ public final class MediaStore { } } + /** @removed */ + @Deprecated + public static @NonNull Set<String> getAllVolumeNames(@NonNull Context context) { + return getExternalVolumeNames(context); + } + /** - * Return list of all volume names currently available. This includes a - * unique name for each shared storage device that is currently mounted. + * Return list of all specific volume names that make up + * {@link #VOLUME_EXTERNAL}. This includes a unique volume name for each + * shared storage device that is currently attached, which typically + * includes {@link MediaStore#VOLUME_EXTERNAL_PRIMARY}. * <p> - * Each name can be passed to APIs like - * {@link MediaStore.Images.Media#getContentUri(String)} to query media at - * that location. + * Each specific volume name can be passed to APIs like + * {@link MediaStore.Images.Media#getContentUri(String)} to interact with + * media on that storage device. */ - public static @NonNull Set<String> getAllVolumeNames(@NonNull Context context) { + public static @NonNull Set<String> getExternalVolumeNames(@NonNull Context context) { final StorageManager sm = context.getSystemService(StorageManager.class); final Set<String> volumeNames = new ArraySet<>(); - volumeNames.add(VOLUME_INTERNAL); for (VolumeInfo vi : sm.getVolumes()) { if (vi.isVisibleForUser(UserHandle.myUserId()) && vi.isMountedReadable()) { if (vi.isPrimary()) { - volumeNames.add(VOLUME_EXTERNAL); + volumeNames.add(VOLUME_EXTERNAL_PRIMARY); } else { volumeNames.add(vi.getNormalizedFsUuid()); } @@ -3270,6 +3307,8 @@ public final class MediaStore { return volumeName; } else if (VOLUME_EXTERNAL.equals(volumeName)) { return volumeName; + } else if (VOLUME_EXTERNAL_PRIMARY.equals(volumeName)) { + return volumeName; } // When not one of the well-known values above, it must be a hex UUID @@ -3285,8 +3324,9 @@ public final class MediaStore { } /** - * Return path where the given volume is mounted. Not valid for - * {@link #VOLUME_INTERNAL}. + * Return path where the given specific volume is mounted. Not valid for + * {@link #VOLUME_INTERNAL} or {@link #VOLUME_EXTERNAL}, since those are + * broad collections that cover many paths. * * @hide */ @@ -3297,8 +3337,12 @@ public final class MediaStore { throw new IllegalArgumentException(); } - if (VOLUME_EXTERNAL.equals(volumeName)) { - return Environment.getExternalStorageDirectory(); + switch (volumeName) { + case VOLUME_INTERNAL: + case VOLUME_EXTERNAL: + throw new FileNotFoundException(volumeName + " has no associated path"); + case VOLUME_EXTERNAL_PRIMARY: + return Environment.getExternalStorageDirectory(); } final StorageManager sm = AppGlobals.getInitialApplication() @@ -3328,23 +3372,31 @@ public final class MediaStore { throw new IllegalArgumentException(); } + final Context context = AppGlobals.getInitialApplication(); + final UserManager um = context.getSystemService(UserManager.class); + final ArrayList<File> res = new ArrayList<>(); if (VOLUME_INTERNAL.equals(volumeName)) { - addCanoncialFile(res, new File(Environment.getRootDirectory(), "media")); - addCanoncialFile(res, new File(Environment.getOemDirectory(), "media")); - addCanoncialFile(res, new File(Environment.getProductDirectory(), "media")); + addCanonicalFile(res, new File(Environment.getRootDirectory(), "media")); + addCanonicalFile(res, new File(Environment.getOemDirectory(), "media")); + addCanonicalFile(res, new File(Environment.getProductDirectory(), "media")); + } else if (VOLUME_EXTERNAL.equals(volumeName)) { + for (String exactVolume : getExternalVolumeNames(context)) { + addCanonicalFile(res, getVolumePath(exactVolume)); + } + if (um.isDemoUser()) { + addCanonicalFile(res, Environment.getDataPreloadsMediaDirectory()); + } } else { - addCanoncialFile(res, getVolumePath(volumeName)); - final UserManager um = AppGlobals.getInitialApplication() - .getSystemService(UserManager.class); - if (VOLUME_EXTERNAL.equals(volumeName) && um.isDemoUser()) { - addCanoncialFile(res, Environment.getDataPreloadsMediaDirectory()); + addCanonicalFile(res, getVolumePath(volumeName)); + if (VOLUME_EXTERNAL_PRIMARY.equals(volumeName) && um.isDemoUser()) { + addCanonicalFile(res, Environment.getDataPreloadsMediaDirectory()); } } return res; } - private static void addCanoncialFile(List<File> list, File file) { + private static void addCanonicalFile(List<File> list, File file) { try { list.add(file.getCanonicalFile()); } catch (IOException e) { @@ -3382,12 +3434,12 @@ public final class MediaStore { * <p> * No other assumptions should be made about the meaning of the version. * <p> - * This method returns the version for {@link MediaStore#VOLUME_EXTERNAL}; - * to obtain a version for a different volume, use - * {@link #getVersion(Context, String)}. + * This method returns the version for + * {@link MediaStore#VOLUME_EXTERNAL_PRIMARY}; to obtain a version for a + * different volume, use {@link #getVersion(Context, String)}. */ public static @NonNull String getVersion(@NonNull Context context) { - return getVersion(context, VOLUME_EXTERNAL); + return getVersion(context, VOLUME_EXTERNAL_PRIMARY); } /** @@ -3401,7 +3453,7 @@ public final class MediaStore { * * @param volumeName specific volume to obtain an opaque version string for. * Must be one of the values returned from - * {@link #getAllVolumeNames(Context)}. + * {@link #getExternalVolumeNames(Context)}. */ public static @NonNull String getVersion(@NonNull Context context, @NonNull String volumeName) { final ContentResolver resolver = context.getContentResolver(); |