diff options
23 files changed, 359 insertions, 661 deletions
diff --git a/api/current.txt b/api/current.txt index 6ddd847435f4..7c0a134063c1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -36025,7 +36025,8 @@ package android.os.storage { method @WorkerThread public long getCacheSizeBytes(@NonNull java.util.UUID) throws java.io.IOException; method public String getMountedObbPath(String); method @NonNull public android.os.storage.StorageVolume getPrimaryStorageVolume(); - method @Nullable public android.os.storage.StorageVolume getStorageVolume(java.io.File); + method @NonNull public java.util.List<android.os.storage.StorageVolume> getRecentStorageVolumes(); + method @Nullable public android.os.storage.StorageVolume getStorageVolume(@NonNull java.io.File); method @NonNull public android.os.storage.StorageVolume getStorageVolume(@NonNull android.net.Uri); method @NonNull public java.util.List<android.os.storage.StorageVolume> getStorageVolumes(); method @NonNull public java.util.UUID getUuidForPath(@NonNull java.io.File) throws java.io.IOException; @@ -36051,6 +36052,8 @@ package android.os.storage { method @NonNull public android.content.Intent createOpenDocumentTreeIntent(); method public int describeContents(); method public String getDescription(android.content.Context); + method @Nullable public java.io.File getDirectory(); + method @Nullable public String getMediaStoreVolumeName(); method public String getState(); method @Nullable public String getUuid(); method public boolean isEmulated(); @@ -38721,6 +38724,7 @@ package android.provider { 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 java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context); method public static boolean getRequireOriginal(@NonNull android.net.Uri); method @NonNull public static String getVersion(@NonNull android.content.Context); method @NonNull public static String getVersion(@NonNull android.content.Context, @NonNull String); @@ -38986,6 +38990,7 @@ package android.provider { method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long); method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long); method @Deprecated public static android.net.Uri getContentUri(String); + method @Deprecated @NonNull public static android.util.Size getKindSize(int); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); method @Deprecated public static final android.database.Cursor query(android.content.ContentResolver, android.net.Uri, String[]); @@ -39068,6 +39073,7 @@ package android.provider { method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long); method @Deprecated public static void cancelThumbnailRequest(android.content.ContentResolver, long, long); method @Deprecated public static android.net.Uri getContentUri(String); + method @Deprecated @NonNull public static android.util.Size getKindSize(int); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); method @Deprecated public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); field @Deprecated public static final String DATA = "_data"; diff --git a/api/removed.txt b/api/removed.txt index 4e8e325bce15..8b30d0a5cf39 100644 --- a/api/removed.txt +++ b/api/removed.txt @@ -436,10 +436,8 @@ 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 public static boolean getIncludePending(@NonNull android.net.Uri); - 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); method @Deprecated public static void trash(@NonNull android.content.Context, @NonNull android.net.Uri, long); @@ -473,22 +471,6 @@ package android.provider { field @Deprecated public static final String GROUP_ID = "group_id"; } - @Deprecated public static class MediaStore.PendingParams { - ctor public MediaStore.PendingParams(@NonNull android.net.Uri, @NonNull String, @NonNull String); - method public void setDownloadUri(@Nullable android.net.Uri); - method public void setRefererUri(@Nullable android.net.Uri); - method public void setRelativePath(@Nullable String); - } - - @Deprecated public static class MediaStore.PendingSession implements java.lang.AutoCloseable { - method public void abandon(); - method public void close(); - method public void notifyProgress(@IntRange(from=0, to=100) int); - method @NonNull public android.os.ParcelFileDescriptor open() throws java.io.FileNotFoundException; - method @NonNull public java.io.OutputStream openOutputStream() throws java.io.FileNotFoundException; - method @NonNull public android.net.Uri publish(); - } - public static interface MediaStore.Video.VideoColumns extends android.provider.MediaStore.MediaColumns { field public static final String ALBUM = "album"; field public static final String ARTIST = "artist"; diff --git a/api/system-current.txt b/api/system-current.txt index 8d55a33faedd..8943a155d199 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6365,6 +6365,7 @@ package android.os { } public class Environment { + method @NonNull public static java.util.Collection<java.io.File> getInternalMediaDirectories(); method @NonNull public static java.io.File getOdmDirectory(); method @NonNull public static java.io.File getOemDirectory(); method @NonNull public static java.io.File getProductDirectory(); @@ -7253,8 +7254,8 @@ package android.provider { } public final class MediaStore { - method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static java.util.Set<java.lang.String> getRecentExternalVolumeNames(@NonNull android.content.Context); - method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static java.util.Collection<java.io.File> getVolumeScanPaths(@NonNull String) throws java.io.FileNotFoundException; + method @NonNull public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File); + method public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String); } public abstract class SearchIndexableData { diff --git a/api/test-current.txt b/api/test-current.txt index 1c6bce05b8f8..5381376d7568 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2467,14 +2467,9 @@ package android.provider { } public final class MediaStore { - method @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) public static void deleteContributedMedia(android.content.Context, String, android.os.UserHandle) throws java.io.IOException; - method @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) public static long getContributedMediaSize(android.content.Context, String, android.os.UserHandle) throws java.io.IOException; - method @NonNull public static java.io.File getVolumePath(@NonNull String) throws java.io.FileNotFoundException; - method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static java.util.Collection<java.io.File> getVolumeScanPaths(@NonNull String) throws java.io.FileNotFoundException; - method public static android.net.Uri scanFile(android.content.Context, java.io.File); - method public static android.net.Uri scanFileFromShell(android.content.Context, java.io.File); - method public static void scanVolume(android.content.Context, java.io.File); - method public static void waitForIdle(android.content.Context); + method @NonNull public static android.net.Uri scanFile(@NonNull android.content.ContentResolver, @NonNull java.io.File); + method public static void scanVolume(@NonNull android.content.ContentResolver, @NonNull String); + method public static void waitForIdle(@NonNull android.content.ContentResolver); } public final class Settings { diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index 80c9ba2a9c48..eb50581f3319 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -1314,8 +1314,8 @@ public class DownloadManager { // TODO: DownloadProvider.update() should take care of updating corresponding // MediaProvider entries. - MediaStore.scanFile(context, before); - MediaStore.scanFile(context, after); + MediaStore.scanFile(mResolver, before); + MediaStore.scanFile(mResolver, after); final ContentValues values = new ContentValues(); values.put(Downloads.Impl.COLUMN_TITLE, displayName); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 94af5416aa8d..0d1f4046e70e 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1513,16 +1513,6 @@ public abstract class PackageManager { public static final int DELETE_DONT_KILL_APP = 0x00000008; /** - * Flag parameter for {@link #deletePackage} to indicate that any - * contributed media should also be deleted during this uninstall. The - * meaning of "contributed" means it won't automatically be deleted when the - * app is uninstalled. - * - * @hide - */ - public static final int DELETE_CONTRIBUTED_MEDIA = 0x00000010; - - /** * Flag parameter for {@link #deletePackage} to indicate that package deletion * should be chatty. * diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index a92237b9f17f..61da5e67e57b 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -34,6 +34,8 @@ import android.text.TextUtils; import android.util.Log; import java.io.File; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedList; /** @@ -528,6 +530,22 @@ public class Environment { } /** + * Return locations where media files (such as ringtones, notification + * sounds, or alarm sounds) may be located on internal storage. These are + * typically indexed under {@link MediaStore#VOLUME_INTERNAL}. + * + * @hide + */ + @SystemApi + public static @NonNull Collection<File> getInternalMediaDirectories() { + final ArrayList<File> res = new ArrayList<>(); + res.add(new File(Environment.getRootDirectory(), "media")); + res.add(new File(Environment.getOemDirectory(), "media")); + res.add(new File(Environment.getProductDirectory(), "media")); + return res; + } + + /** * Return the primary shared/external storage directory. This directory may * not currently be accessible if it has been mounted by the user on their * computer, has been removed from the device, or some other problem has diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 62603fee1137..2e9f27e74544 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -264,6 +264,8 @@ public class StorageManager { public static final int FLAG_REAL_STATE = 1 << 9; /** {@hide} */ public static final int FLAG_INCLUDE_INVISIBLE = 1 << 10; + /** {@hide} */ + public static final int FLAG_INCLUDE_RECENT = 1 << 11; /** {@hide} */ public static final int FSTRIM_FLAG_DEEP = IVold.FSTRIM_FLAG_DEEP_TRIM; @@ -1125,7 +1127,7 @@ public class StorageManager { * Return the {@link StorageVolume} that contains the given file, or * {@code null} if none. */ - public @Nullable StorageVolume getStorageVolume(File file) { + public @Nullable StorageVolume getStorageVolume(@NonNull File file) { return getStorageVolume(getVolumeList(), file); } @@ -1140,7 +1142,7 @@ public class StorageManager { return getPrimaryStorageVolume(); default: for (StorageVolume vol : getStorageVolumes()) { - if (Objects.equals(vol.getNormalizedUuid(), volumeName)) { + if (Objects.equals(vol.getMediaStoreVolumeName(), volumeName)) { return vol; } } @@ -1201,12 +1203,13 @@ public class StorageManager { } /** - * Return the list of shared/external storage volumes available to the - * current user. This includes both the primary shared storage device and - * any attached external volumes including SD cards and USB drives. - * - * @see Environment#getExternalStorageDirectory() - * @see StorageVolume#createAccessIntent(String) + * Return the list of shared/external storage volumes currently available to + * the calling user. + * <p> + * These storage volumes are actively attached to the device, but may be in + * any mount state, as returned by {@link StorageVolume#getState()}. Returns + * both the primary shared storage device and any attached external volumes, + * including SD cards and USB drives. */ public @NonNull List<StorageVolume> getStorageVolumes() { final ArrayList<StorageVolume> res = new ArrayList<>(); @@ -1216,6 +1219,22 @@ public class StorageManager { } /** + * Return the list of shared/external storage volumes both currently and + * recently available to the calling user. + * <p> + * Recently available storage volumes are likely to reappear in the future, + * so apps are encouraged to preserve any indexed metadata related to these + * volumes to optimize user experiences. + */ + public @NonNull List<StorageVolume> getRecentStorageVolumes() { + final ArrayList<StorageVolume> res = new ArrayList<>(); + Collections.addAll(res, + getVolumeList(mContext.getUserId(), + FLAG_REAL_STATE | FLAG_INCLUDE_INVISIBLE | FLAG_INCLUDE_RECENT)); + return res; + } + + /** * Return the primary shared/external storage volume available to the * current user. This volume is the same storage device returned by * {@link Environment#getExternalStorageDirectory()} and diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index aefe8430f9de..560d6171d5ee 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -29,6 +29,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; import android.provider.DocumentsContract; +import android.provider.MediaStore; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; @@ -173,7 +174,7 @@ public final class StorageVolume implements Parcelable { * @return the mount path * @hide */ - @UnsupportedAppUsage + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}") @TestApi public String getPath() { return mPath.toString(); @@ -190,12 +191,35 @@ public final class StorageVolume implements Parcelable { } /** {@hide} */ - @UnsupportedAppUsage + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.Q, publicAlternatives = "{@link StorageVolume#getDirectory()}") public File getPathFile() { return mPath; } /** + * Returns the directory where this volume is currently mounted. + * <p> + * Direct filesystem access via this path has significant emulation + * overhead, and apps are instead strongly encouraged to interact with media + * on storage volumes via the {@link MediaStore} APIs. + * <p> + * This directory does not give apps any additional access beyond what they + * already have via {@link MediaStore}. + * + * @return directory where this volume is mounted, or {@code null} if the + * volume is not currently mounted. + */ + public @Nullable File getDirectory() { + switch (mState) { + case Environment.MEDIA_MOUNTED: + case Environment.MEDIA_MOUNTED_READ_ONLY: + return mPath; + default: + return null; + } + } + + /** * Returns a user-visible description of the volume. * * @return the volume description @@ -265,6 +289,24 @@ public final class StorageVolume implements Parcelable { return mFsUuid; } + /** + * Return the volume name that can be used to interact with this storage + * device through {@link MediaStore}. + * + * @return opaque volume name, or {@code null} if this volume is not indexed + * by {@link MediaStore}. + * @see android.provider.MediaStore.Audio.Media#getContentUri(String) + * @see android.provider.MediaStore.Video.Media#getContentUri(String) + * @see android.provider.MediaStore.Images.Media#getContentUri(String) + */ + public @Nullable String getMediaStoreVolumeName() { + if (isPrimary()) { + return MediaStore.VOLUME_EXTERNAL_PRIMARY; + } else { + return getNormalizedUuid(); + } + } + /** {@hide} */ public static @Nullable String normalizeUuid(@Nullable String fsUuid) { return fsUuid != null ? fsUuid.toLowerCase(Locale.US) : null; diff --git a/core/java/android/os/storage/VolumeRecord.java b/core/java/android/os/storage/VolumeRecord.java index 1a794ebf2a59..99b45d60c319 100644 --- a/core/java/android/os/storage/VolumeRecord.java +++ b/core/java/android/os/storage/VolumeRecord.java @@ -17,14 +17,18 @@ package android.os.storage; import android.annotation.UnsupportedAppUsage; +import android.content.Context; +import android.os.Environment; import android.os.Parcel; import android.os.Parcelable; +import android.os.UserHandle; import android.util.DebugUtils; import android.util.TimeUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; +import java.io.File; import java.util.Locale; import java.util.Objects; @@ -92,6 +96,27 @@ public class VolumeRecord implements Parcelable { return (userFlags & USER_FLAG_SNOOZED) != 0; } + public StorageVolume buildStorageVolume(Context context) { + final String id = "unknown:" + fsUuid; + final File userPath = new File("/dev/null"); + final File internalPath = new File("/dev/null"); + final boolean primary = false; + final boolean removable = true; + final boolean emulated = false; + final boolean allowMassStorage = false; + final long maxFileSize = 0; + final UserHandle user = new UserHandle(UserHandle.USER_NULL); + final String envState = Environment.MEDIA_UNKNOWN; + + String description = nickname; + if (description == null) { + description = context.getString(android.R.string.unknownName); + } + + return new StorageVolume(id, userPath, internalPath, description, primary, removable, + emulated, allowMassStorage, maxFileSize, user, fsUuid, envState); + } + public void dump(IndentingPrintWriter pw) { pw.println("VolumeRecord:"); pw.increaseIndent(); diff --git a/core/java/android/provider/BaseColumns.java b/core/java/android/provider/BaseColumns.java index 00c9e72df880..b216e2b7ed8d 100644 --- a/core/java/android/provider/BaseColumns.java +++ b/core/java/android/provider/BaseColumns.java @@ -16,13 +16,11 @@ package android.provider; -import android.database.Cursor; - public interface BaseColumns { /** * The unique ID for a row. */ - @Column(Cursor.FIELD_TYPE_INTEGER) + // @Column(Cursor.FIELD_TYPE_INTEGER) public static final String _ID = "_id"; /** diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 701ba91a8daf..63204d36f396 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -21,17 +21,15 @@ import android.annotation.CurrentTimeMillisLong; import android.annotation.CurrentTimeSecondsLong; import android.annotation.DurationMillisLong; import android.annotation.IntDef; -import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UnsupportedAppUsage; import android.app.Activity; -import android.app.AppGlobals; import android.app.PendingIntent; import android.content.ClipData; import android.content.ContentProviderClient; @@ -45,39 +43,28 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageDecoder; -import android.graphics.Point; import android.graphics.PostProcessor; import android.media.ExifInterface; -import android.media.MediaFile; import android.media.MediaFormat; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Environment; -import android.os.FileUtils; import android.os.OperationCanceledException; -import android.os.ParcelFileDescriptor; import android.os.RemoteException; -import android.os.UserHandle; -import android.os.UserManager; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; -import android.os.storage.VolumeInfo; -import android.os.storage.VolumeRecord; -import android.service.media.CameraPrewarmService; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; - -import com.android.internal.annotations.GuardedBy; +import android.util.Size; import libcore.util.HexEncoding; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -162,9 +149,6 @@ public final class MediaStore { /** {@hide} */ public static final String SCAN_VOLUME_CALL = "scan_volume"; /** {@hide} */ - public static final String SUICIDE_CALL = "suicide"; - - /** {@hide} */ public static final String CREATE_WRITE_REQUEST_CALL = "create_write_request"; /** {@hide} */ public static final String CREATE_TRASH_REQUEST_CALL = "create_trash_request"; @@ -173,42 +157,19 @@ public final class MediaStore { /** {@hide} */ public static final String CREATE_DELETE_REQUEST_CALL = "create_delete_request"; - /** - * Extra used with {@link #SCAN_FILE_CALL} or {@link #SCAN_VOLUME_CALL} to indicate that - * the file path originated from shell. - * - * {@hide} - */ - public static final String EXTRA_ORIGINATED_FROM_SHELL = - "android.intent.extra.originated_from_shell"; - - /** - * The method name used by the media scanner and mtp to tell the media provider to - * rescan and reclassify that have become unhidden because of renaming folders or - * removing nomedia files - * @hide - */ - @Deprecated - public static final String UNHIDE_CALL = "unhide"; - - /** - * The method name used by the media scanner service to reload all localized ringtone titles due - * to a locale change. - * @hide - */ - public static final String RETRANSLATE_CALL = "update_titles"; /** {@hide} */ public static final String GET_VERSION_CALL = "get_version"; + /** {@hide} */ public static final String GET_DOCUMENT_URI_CALL = "get_document_uri"; /** {@hide} */ public static final String GET_MEDIA_URI_CALL = "get_media_uri"; /** {@hide} */ - public static final String GET_CONTRIBUTED_MEDIA_CALL = "get_contributed_media"; + public static final String EXTRA_URI = "uri"; /** {@hide} */ - public static final String DELETE_CONTRIBUTED_MEDIA_CALL = "delete_contributed_media"; + public static final String EXTRA_URI_PERMISSIONS = "uriPermissions"; /** {@hide} */ public static final String EXTRA_CLIP_DATA = "clip_data"; @@ -391,10 +352,10 @@ public final class MediaStore { * service. * <p> * This meta-data should reference the fully qualified class name of the prewarm service - * extending {@link CameraPrewarmService}. + * extending {@code CameraPrewarmService}. * <p> * The prewarm service will get bound and receive a prewarm signal - * {@link CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent. + * {@code CameraPrewarmService#onPrewarm()} when a camera launch intent fire might be imminent. * An application implementing a prewarm service should do the absolute minimum amount of work * to initialize the camera in order to reduce startup time in likely case that shortly after a * camera launch intent would be sent. @@ -775,197 +736,6 @@ public final class MediaStore { } /** - * Create a new pending media item using the given parameters. Pending items - * are expected to have a short lifetime, and owners should either - * {@link PendingSession#publish()} or {@link PendingSession#abandon()} a - * pending item within a few hours after first creating it. - * - * @return token which can be passed to {@link #openPending(Context, Uri)} - * to work with this pending item. - * @see MediaColumns#IS_PENDING - * @see MediaStore#setIncludePending(Uri) - * @see MediaStore#createPending(Context, PendingParams) - * @removed - */ - @Deprecated - public static @NonNull Uri createPending(@NonNull Context context, - @NonNull PendingParams params) { - return context.getContentResolver().insert(params.insertUri, params.insertValues); - } - - /** - * Open a pending media item to make progress on it. You can open a pending - * item multiple times before finally calling either - * {@link PendingSession#publish()} or {@link PendingSession#abandon()}. - * - * @param uri token which was previously returned from - * {@link #createPending(Context, PendingParams)}. - * @removed - */ - @Deprecated - public static @NonNull PendingSession openPending(@NonNull Context context, @NonNull Uri uri) { - return new PendingSession(context, uri); - } - - /** - * Parameters that describe a pending media item. - * - * @removed - */ - @Deprecated - public static class PendingParams { - /** {@hide} */ - public final Uri insertUri; - /** {@hide} */ - public final ContentValues insertValues; - - /** - * Create parameters that describe a pending media item. - * - * @param insertUri the {@code content://} Uri where this pending item - * should be inserted when finally published. For example, to - * publish an image, use - * {@link MediaStore.Images.Media#getContentUri(String)}. - */ - public PendingParams(@NonNull Uri insertUri, @NonNull String displayName, - @NonNull String mimeType) { - this.insertUri = Objects.requireNonNull(insertUri); - final long now = System.currentTimeMillis() / 1000; - this.insertValues = new ContentValues(); - this.insertValues.put(MediaColumns.DISPLAY_NAME, Objects.requireNonNull(displayName)); - this.insertValues.put(MediaColumns.MIME_TYPE, Objects.requireNonNull(mimeType)); - this.insertValues.put(MediaColumns.DATE_ADDED, now); - this.insertValues.put(MediaColumns.DATE_MODIFIED, now); - this.insertValues.put(MediaColumns.IS_PENDING, 1); - this.insertValues.put(MediaColumns.DATE_EXPIRES, - (System.currentTimeMillis() + DateUtils.DAY_IN_MILLIS) / 1000); - } - - public void setRelativePath(@Nullable String relativePath) { - if (relativePath == null) { - this.insertValues.remove(MediaColumns.RELATIVE_PATH); - } else { - this.insertValues.put(MediaColumns.RELATIVE_PATH, relativePath); - } - } - - /** - * Optionally set the Uri from where the file has been downloaded. This is used - * for files being added to {@link Downloads} table. - * - * @see DownloadColumns#DOWNLOAD_URI - */ - public void setDownloadUri(@Nullable Uri downloadUri) { - if (downloadUri == null) { - this.insertValues.remove(DownloadColumns.DOWNLOAD_URI); - } else { - this.insertValues.put(DownloadColumns.DOWNLOAD_URI, downloadUri.toString()); - } - } - - /** - * Optionally set the Uri indicating HTTP referer of the file. This is used for - * files being added to {@link Downloads} table. - * - * @see DownloadColumns#REFERER_URI - */ - public void setRefererUri(@Nullable Uri refererUri) { - if (refererUri == null) { - this.insertValues.remove(DownloadColumns.REFERER_URI); - } else { - this.insertValues.put(DownloadColumns.REFERER_URI, refererUri.toString()); - } - } - } - - /** - * Session actively working on a pending media item. Pending items are - * expected to have a short lifetime, and owners should either - * {@link PendingSession#publish()} or {@link PendingSession#abandon()} a - * pending item within a few hours after first creating it. - * - * @removed - */ - @Deprecated - public static class PendingSession implements AutoCloseable { - /** {@hide} */ - private final Context mContext; - /** {@hide} */ - private final Uri mUri; - - /** {@hide} */ - public PendingSession(Context context, Uri uri) { - mContext = Objects.requireNonNull(context); - mUri = Objects.requireNonNull(uri); - } - - /** - * Open the underlying file representing this media item. When a media - * item is successfully completed, you should - * {@link ParcelFileDescriptor#close()} and then {@link #publish()} it. - * - * @see #notifyProgress(int) - */ - public @NonNull ParcelFileDescriptor open() throws FileNotFoundException { - return mContext.getContentResolver().openFileDescriptor(mUri, "rw"); - } - - /** - * Open the underlying file representing this media item. When a media - * item is successfully completed, you should - * {@link OutputStream#close()} and then {@link #publish()} it. - * - * @see #notifyProgress(int) - */ - public @NonNull OutputStream openOutputStream() throws FileNotFoundException { - return mContext.getContentResolver().openOutputStream(mUri); - } - - /** - * Notify of current progress on this pending media item. Gallery - * applications may choose to surface progress information of this - * pending item. - * - * @param progress a percentage between 0 and 100. - */ - public void notifyProgress(@IntRange(from = 0, to = 100) int progress) { - final Uri withProgress = mUri.buildUpon() - .appendQueryParameter(PARAM_PROGRESS, Integer.toString(progress)).build(); - mContext.getContentResolver().notifyChange(withProgress, null, 0); - } - - /** - * When this media item is successfully completed, call this method to - * publish and make the final item visible to the user. - * - * @return the final {@code content://} Uri representing the newly - * published media. - */ - public @NonNull Uri publish() { - final ContentValues values = new ContentValues(); - values.put(MediaColumns.IS_PENDING, 0); - values.putNull(MediaColumns.DATE_EXPIRES); - mContext.getContentResolver().update(mUri, values, null, null); - return mUri; - } - - /** - * When this media item has failed to be completed, call this method to - * destroy the pending item record and any data related to it. - */ - public void abandon() { - mContext.getContentResolver().delete(mUri, null, null); - } - - @Override - public void close() { - // No resources to close, but at least we can inform people that no - // progress is being actively made. - notifyProgress(-1); - } - } - - /** * Mark the given item as being "trashed", meaning it should be deleted at * some point in the future. This is a more gentle operation than simply * calling {@link ContentResolver#delete(Uri, String, String[])}, which @@ -1701,34 +1471,22 @@ public final class MediaStore { return ContentUris.withAppendedId(getContentUri(volumeName), rowId); } - /** - * For use only by the MTP implementation. - * @hide - */ + /** {@hide} */ @UnsupportedAppUsage - public static Uri getMtpObjectsUri(String volumeName) { - return AUTHORITY_URI.buildUpon().appendPath(volumeName).appendPath("object").build(); + public static Uri getMtpObjectsUri(@NonNull String volumeName) { + return MediaStore.Files.getContentUri(volumeName); } - /** - * For use only by the MTP implementation. - * @hide - */ + /** {@hide} */ @UnsupportedAppUsage - public static final Uri getMtpObjectsUri(String volumeName, - long fileId) { - return ContentUris.withAppendedId(getMtpObjectsUri(volumeName), fileId); + public static final Uri getMtpObjectsUri(@NonNull String volumeName, long fileId) { + return MediaStore.Files.getContentUri(volumeName, fileId); } - /** - * Used to implement the MTP GetObjectReferences and SetObjectReferences commands. - * @hide - */ + /** {@hide} */ @UnsupportedAppUsage - public static final Uri getMtpReferencesUri(String volumeName, - long fileId) { - return getMtpObjectsUri(volumeName, fileId).buildUpon().appendPath("references") - .build(); + public static final Uri getMtpReferencesUri(@NonNull String volumeName, long fileId) { + return MediaStore.Files.getContentUri(volumeName, fileId); } /** @@ -1851,9 +1609,21 @@ public final class MediaStore { public static final int FULL_SCREEN_KIND = 2; public static final int MICRO_KIND = 3; - public static final Point MINI_SIZE = new Point(512, 384); - public static final Point FULL_SCREEN_SIZE = new Point(1024, 786); - public static final Point MICRO_SIZE = new Point(96, 96); + public static final Size MINI_SIZE = new Size(512, 384); + public static final Size FULL_SCREEN_SIZE = new Size(1024, 786); + public static final Size MICRO_SIZE = new Size(96, 96); + + public static @NonNull Size getKindSize(int kind) { + if (kind == ThumbnailConstants.MICRO_KIND) { + return ThumbnailConstants.MICRO_SIZE; + } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) { + return ThumbnailConstants.FULL_SCREEN_SIZE; + } else if (kind == ThumbnailConstants.MINI_KIND) { + return ThumbnailConstants.MINI_SIZE; + } else { + throw new IllegalArgumentException("Unsupported kind: " + kind); + } + } } /** @@ -1957,22 +1727,22 @@ public final class MediaStore { } } - /** {@hide} */ + /** + * @deprecated since this method doesn't have a {@link Context}, we can't + * find the actual {@link StorageVolume} for the given path, so + * only a vague guess is returned. Callers should use + * {@link StorageManager#getStorageVolume(File)} instead. + * @hide + */ + @Deprecated public static @NonNull String getVolumeName(@NonNull File path) { - if (FileUtils.contains(Environment.getStorageDirectory(), path)) { - final StorageManager sm = AppGlobals.getInitialApplication() - .getSystemService(StorageManager.class); - final StorageVolume sv = sm.getStorageVolume(path); - if (sv != null) { - if (sv.isPrimary()) { - return VOLUME_EXTERNAL_PRIMARY; - } else { - return checkArgumentVolumeName(sv.getNormalizedUuid()); - } - } - throw new IllegalStateException("Unknown volume at " + path); + // Ideally we'd find the relevant StorageVolume, but we don't have a + // Context to obtain it from, so the best we can do is assume + if (path.getAbsolutePath() + .startsWith(Environment.getStorageDirectory().getAbsolutePath())) { + return MediaStore.VOLUME_EXTERNAL; } else { - return VOLUME_INTERNAL; + return MediaStore.VOLUME_INTERNAL; } } @@ -1985,7 +1755,7 @@ public final class MediaStore { /** * Currently outstanding thumbnail requests that can be cancelled. */ - @GuardedBy("sPending") + // @GuardedBy("sPending") private static ArrayMap<Uri, CancellationSignal> sPending = new ArrayMap<>(); /** @@ -1997,16 +1767,7 @@ public final class MediaStore { @Deprecated static @Nullable Bitmap getThumbnail(@NonNull ContentResolver cr, @NonNull Uri uri, int kind, @Nullable BitmapFactory.Options opts) { - final Point size; - if (kind == ThumbnailConstants.MICRO_KIND) { - size = ThumbnailConstants.MICRO_SIZE; - } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) { - size = ThumbnailConstants.FULL_SCREEN_SIZE; - } else if (kind == ThumbnailConstants.MINI_KIND) { - size = ThumbnailConstants.MINI_SIZE; - } else { - throw new IllegalArgumentException("Unsupported kind: " + kind); - } + final Size size = ThumbnailConstants.getKindSize(kind); CancellationSignal signal = null; synchronized (sPending) { @@ -2018,7 +1779,7 @@ public final class MediaStore { } try { - return cr.loadThumbnail(uri, Point.convert(size), signal); + return cr.loadThumbnail(uri, size, signal); } catch (IOException e) { Log.w(TAG, "Failed to obtain thumbnail for " + uri, e); return null; @@ -2215,26 +1976,14 @@ public final class MediaStore { @Deprecated public static final String insertImage(ContentResolver cr, String imagePath, String name, String description) throws FileNotFoundException { - final File file = new File(imagePath); - final String mimeType = MediaFile.getMimeTypeForFile(imagePath); - - if (TextUtils.isEmpty(name)) name = "Image"; - final PendingParams params = new PendingParams( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, name, mimeType); - - final Context context = AppGlobals.getInitialApplication(); - final Uri pendingUri = createPending(context, params); - try (PendingSession session = openPending(context, pendingUri)) { - try (InputStream in = new FileInputStream(file); - OutputStream out = session.openOutputStream()) { - FileUtils.copy(in, out); - } - return session.publish().toString(); - } catch (Exception e) { - Log.w(TAG, "Failed to insert image", e); - context.getContentResolver().delete(pendingUri, null, null); - return null; + final Bitmap source; + try { + source = ImageDecoder + .decodeBitmap(ImageDecoder.createSource(new File(imagePath))); + } catch (IOException e) { + throw new FileNotFoundException(e.getMessage()); } + return insertImage(cr, source, name, description); } /** @@ -2251,22 +2000,34 @@ public final class MediaStore { * control over lifecycle. */ @Deprecated - public static final String insertImage(ContentResolver cr, Bitmap source, - String title, String description) { + public static final String insertImage(ContentResolver cr, Bitmap source, String title, + String description) { if (TextUtils.isEmpty(title)) title = "Image"; - final PendingParams params = new PendingParams( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, title, "image/jpeg"); - final Context context = AppGlobals.getInitialApplication(); - final Uri pendingUri = createPending(context, params); - try (PendingSession session = openPending(context, pendingUri)) { - try (OutputStream out = session.openOutputStream()) { + final long now = System.currentTimeMillis(); + final ContentValues values = new ContentValues(); + values.put(MediaColumns.DISPLAY_NAME, title); + values.put(MediaColumns.MIME_TYPE, "image/jpeg"); + values.put(MediaColumns.DATE_ADDED, now / 1000); + values.put(MediaColumns.DATE_MODIFIED, now / 1000); + values.put(MediaColumns.DATE_EXPIRES, (now + DateUtils.DAY_IN_MILLIS) / 1000); + values.put(MediaColumns.IS_PENDING, 1); + + final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + try { + try (OutputStream out = cr.openOutputStream(uri)) { source.compress(Bitmap.CompressFormat.JPEG, 90, out); } - return session.publish().toString(); + + // Everything went well above, publish it! + values.clear(); + values.put(MediaColumns.IS_PENDING, 0); + values.putNull(MediaColumns.DATE_EXPIRES); + cr.update(uri, values, null, null); + return uri.toString(); } catch (Exception e) { Log.w(TAG, "Failed to insert image", e); - context.getContentResolver().delete(pendingUri, null, null); + cr.delete(uri, null, null); return null; } } @@ -2526,6 +2287,14 @@ public final class MediaStore { public static final int MICRO_KIND = ThumbnailConstants.MICRO_KIND; /** + * Return the typical {@link Size} (in pixels) used internally when + * the given thumbnail kind is requested. + */ + public static @NonNull Size getKindSize(int kind) { + return ThumbnailConstants.getKindSize(kind); + } + + /** * The blob raw data of thumbnail * * @deprecated this column never existed internally, and could never @@ -3780,6 +3549,14 @@ public final class MediaStore { public static final int MICRO_KIND = ThumbnailConstants.MICRO_KIND; /** + * Return the typical {@link Size} (in pixels) used internally when + * the given thumbnail kind is requested. + */ + public static @NonNull Size getKindSize(int kind) { + return ThumbnailConstants.getKindSize(kind); + } + + /** * The width of the thumbnal */ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true) @@ -3811,54 +3588,44 @@ public final class MediaStore { */ public static @NonNull Set<String> getExternalVolumeNames(@NonNull Context context) { final StorageManager sm = context.getSystemService(StorageManager.class); - final Set<String> volumeNames = new ArraySet<>(); - for (VolumeInfo vi : sm.getVolumes()) { - if (vi.isVisibleForUser(UserHandle.myUserId()) && vi.isMountedReadable()) { - if (vi.isPrimary()) { - volumeNames.add(VOLUME_EXTERNAL_PRIMARY); - } else { - volumeNames.add(vi.getNormalizedFsUuid()); + final Set<String> res = new ArraySet<>(); + for (StorageVolume sv : sm.getStorageVolumes()) { + switch (sv.getState()) { + case Environment.MEDIA_MOUNTED: + case Environment.MEDIA_MOUNTED_READ_ONLY: { + final String volumeName = sv.getMediaStoreVolumeName(); + if (volumeName != null) { + res.add(volumeName); + } + break; } } } - return volumeNames; + return res; } /** - * Return list of all specific volume names that have recently been part of + * Return list of all recent volume names that have been part of * {@link #VOLUME_EXTERNAL}. * <p> - * This includes both currently mounted volumes <em>and</em> recently - * mounted (but currently unmounted) volumes. Any indexed metadata for these - * volumes is preserved to optimize the speed of remounting at a later time. - * - * @hide + * These volume names are not currently mounted, but they're likely to + * reappear in the future, so apps are encouraged to preserve any indexed + * metadata related to these volumes to optimize user experiences. + * <p> + * Each specific volume name can be passed to APIs like + * {@link MediaStore.Images.Media#getContentUri(String)} to interact with + * media on that storage device. */ - @SystemApi - @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) public static @NonNull Set<String> getRecentExternalVolumeNames(@NonNull Context context) { final StorageManager sm = context.getSystemService(StorageManager.class); - - // We always have primary storage - final Set<String> volumeNames = new ArraySet<>(); - volumeNames.add(VOLUME_EXTERNAL_PRIMARY); - - final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS; - for (VolumeRecord rec : sm.getVolumeRecords()) { - // Skip volumes without valid UUIDs - if (TextUtils.isEmpty(rec.fsUuid)) continue; - - final VolumeInfo vi = sm.findVolumeByUuid(rec.fsUuid); - if (vi != null && vi.isVisibleForUser(UserHandle.myUserId()) - && vi.isMountedReadable()) { - // We're mounted right now - volumeNames.add(rec.getNormalizedFsUuid()); - } else if (rec.lastSeenMillis > 0 && rec.lastSeenMillis < lastWeek) { - // We're not mounted right now, but we've been seen recently - volumeNames.add(rec.getNormalizedFsUuid()); + final Set<String> res = new ArraySet<>(); + for (StorageVolume sv : sm.getRecentStorageVolumes()) { + final String volumeName = sv.getMediaStoreVolumeName(); + if (volumeName != null) { + res.add(volumeName); } } - return volumeNames; + return res; } /** @@ -3911,97 +3678,6 @@ public final class MediaStore { } /** - * 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 - */ - @TestApi - public static @NonNull File getVolumePath(@NonNull String volumeName) - throws FileNotFoundException { - final StorageManager sm = AppGlobals.getInitialApplication() - .getSystemService(StorageManager.class); - return getVolumePath(sm.getVolumes(), volumeName); - } - - /** {@hide} */ - public static @NonNull File getVolumePath(@NonNull List<VolumeInfo> volumes, - @NonNull String volumeName) throws FileNotFoundException { - if (TextUtils.isEmpty(volumeName)) { - throw new IllegalArgumentException(); - } - - switch (volumeName) { - case VOLUME_INTERNAL: - case VOLUME_EXTERNAL: - throw new FileNotFoundException(volumeName + " has no associated path"); - } - - final boolean wantPrimary = VOLUME_EXTERNAL_PRIMARY.equals(volumeName); - for (VolumeInfo volume : volumes) { - final boolean matchPrimary = wantPrimary - && volume.isPrimary(); - final boolean matchSecondary = !wantPrimary - && Objects.equals(volume.getNormalizedFsUuid(), volumeName); - if (matchPrimary || matchSecondary) { - final File path = volume.getPathForUser(UserHandle.myUserId()); - if (path != null) { - return path; - } - } - } - throw new FileNotFoundException("Failed to find path for " + volumeName); - } - - /** - * Return paths that should be scanned for the given volume. - * - * @hide - */ - @TestApi - @SystemApi - @RequiresPermission(android.Manifest.permission.WRITE_MEDIA_STORAGE) - public static @NonNull Collection<File> getVolumeScanPaths(@NonNull String volumeName) - throws FileNotFoundException { - if (TextUtils.isEmpty(volumeName)) { - 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)) { - 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 { - addCanonicalFile(res, getVolumePath(volumeName)); - if (VOLUME_EXTERNAL_PRIMARY.equals(volumeName) && um.isDemoUser()) { - addCanonicalFile(res, Environment.getDataPreloadsMediaDirectory()); - } - } - return res; - } - - private static void addCanonicalFile(List<File> list, File file) { - try { - list.add(file.getCanonicalFile()); - } catch (IOException e) { - Log.w(TAG, "Failed to resolve " + file + ": " + e); - list.add(file); - } - } - - /** * Uri for querying the state of the media scanner. */ public static Uri getMediaScannerUri() { @@ -4084,10 +3760,10 @@ public final class MediaStore { try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { final Bundle in = new Bundle(); - in.putParcelable(DocumentsContract.EXTRA_URI, mediaUri); - in.putParcelableList(DocumentsContract.EXTRA_URI_PERMISSIONS, uriPermissions); + in.putParcelable(EXTRA_URI, mediaUri); + in.putParcelableArrayList(EXTRA_URI_PERMISSIONS, new ArrayList<>(uriPermissions)); final Bundle out = client.call(GET_DOCUMENT_URI_CALL, null, in); - return out.getParcelable(DocumentsContract.EXTRA_URI); + return out.getParcelable(EXTRA_URI); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } @@ -4114,134 +3790,43 @@ public final class MediaStore { try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { final Bundle in = new Bundle(); - in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); - in.putParcelableList(DocumentsContract.EXTRA_URI_PERMISSIONS, uriPermissions); + in.putParcelable(EXTRA_URI, documentUri); + in.putParcelableArrayList(EXTRA_URI_PERMISSIONS, new ArrayList<>(uriPermissions)); final Bundle out = client.call(GET_MEDIA_URI_CALL, null, in); - return out.getParcelable(DocumentsContract.EXTRA_URI); + return out.getParcelable(EXTRA_URI); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } + /** @hide */ + @TestApi + public static void waitForIdle(@NonNull ContentResolver resolver) { + resolver.call(AUTHORITY, WAIT_FOR_IDLE_CALL, null, null); + } + /** - * Calculate size of media contributed by given package under the calling - * user. The meaning of "contributed" means it won't automatically be - * deleted when the app is uninstalled. + * Perform a blocking scan of the given {@link File}, returning the + * {@link Uri} of the scanned file. * * @hide */ + @SystemApi @TestApi - @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) - public static @BytesLong long getContributedMediaSize(Context context, String packageName, - UserHandle user) throws IOException { - final UserManager um = context.getSystemService(UserManager.class); - if (um.isUserUnlocked(user) && um.isUserRunning(user)) { - try { - final ContentResolver resolver = context - .createPackageContextAsUser(packageName, 0, user).getContentResolver(); - final Bundle in = new Bundle(); - in.putString(Intent.EXTRA_PACKAGE_NAME, packageName); - final Bundle out = resolver.call(AUTHORITY, GET_CONTRIBUTED_MEDIA_CALL, null, in); - return out.getLong(Intent.EXTRA_INDEX); - } catch (Exception e) { - throw new IOException(e); - } - } else { - throw new IOException("User " + user + " must be unlocked and running"); - } + @SuppressLint("StreamFiles") + public static @NonNull Uri scanFile(@NonNull ContentResolver resolver, @NonNull File file) { + final Bundle out = resolver.call(AUTHORITY, SCAN_FILE_CALL, file.getAbsolutePath(), null); + return out.getParcelable(Intent.EXTRA_STREAM); } /** - * Delete all media contributed by given package under the calling user. The - * meaning of "contributed" means it won't automatically be deleted when the - * app is uninstalled. + * Perform a blocking scan of the given storage volume. * * @hide */ + @SystemApi @TestApi - @RequiresPermission(android.Manifest.permission.CLEAR_APP_USER_DATA) - public static void deleteContributedMedia(Context context, String packageName, - UserHandle user) throws IOException { - final UserManager um = context.getSystemService(UserManager.class); - if (um.isUserUnlocked(user) && um.isUserRunning(user)) { - try { - final ContentResolver resolver = context - .createPackageContextAsUser(packageName, 0, user).getContentResolver(); - final Bundle in = new Bundle(); - in.putString(Intent.EXTRA_PACKAGE_NAME, packageName); - resolver.call(AUTHORITY, DELETE_CONTRIBUTED_MEDIA_CALL, null, in); - } catch (Exception e) { - throw new IOException(e); - } - } else { - throw new IOException("User " + user + " must be unlocked and running"); - } - } - - /** @hide */ - @TestApi - public static void waitForIdle(Context context) { - final ContentResolver resolver = context.getContentResolver(); - try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { - client.call(WAIT_FOR_IDLE_CALL, null, null); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } - - /** @hide */ - public static void suicide(Context context) { - final ContentResolver resolver = context.getContentResolver(); - try (ContentProviderClient client = resolver - .acquireUnstableContentProviderClient(AUTHORITY)) { - client.call(SUICIDE_CALL, null, null); - } catch (Exception ignored) { - } - } - - /** @hide */ - @TestApi - public static Uri scanFile(Context context, File file) { - return scan(context, SCAN_FILE_CALL, file, false); - } - - /** @hide */ - @TestApi - public static Uri scanFileFromShell(Context context, File file) { - return scan(context, SCAN_FILE_CALL, file, true); - } - - /** @hide */ - @TestApi - public static void scanVolume(Context context, File file) { - scan(context, SCAN_VOLUME_CALL, file, false); - } - - /** @hide */ - public static Uri scanFile(ContentProviderClient client, File file) { - return scan(client, SCAN_FILE_CALL, file, false); - } - - /** @hide */ - private static Uri scan(Context context, String method, File file, - boolean originatedFromShell) { - final ContentResolver resolver = context.getContentResolver(); - try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) { - return scan(client, method, file, originatedFromShell); - } - } - - /** @hide */ - private static Uri scan(ContentProviderClient client, String method, File file, - boolean originatedFromShell) { - try { - final Bundle in = new Bundle(); - in.putParcelable(Intent.EXTRA_STREAM, Uri.fromFile(file)); - in.putBoolean(EXTRA_ORIGINATED_FROM_SHELL, originatedFromShell); - final Bundle out = client.call(method, null, in); - return out.getParcelable(Intent.EXTRA_STREAM); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } + public static void scanVolume(@NonNull ContentResolver resolver, @NonNull String volumeName) { + resolver.call(AUTHORITY, SCAN_VOLUME_CALL, volumeName, null); } } diff --git a/core/java/com/android/internal/content/FileSystemProvider.java b/core/java/com/android/internal/content/FileSystemProvider.java index f5708a5c89af..dec9ae701fb2 100644 --- a/core/java/com/android/internal/content/FileSystemProvider.java +++ b/core/java/com/android/internal/content/FileSystemProvider.java @@ -259,7 +259,7 @@ public abstract class FileSystemProvider extends DocumentsProvider { throw new IllegalStateException("Failed to touch " + file + ": " + e); } } - MediaStore.scanFile(getContext(), file); + MediaStore.scanFile(getContext().getContentResolver(), file); return childId; } @@ -316,10 +316,10 @@ public abstract class FileSystemProvider extends DocumentsProvider { private void moveInMediaStore(@Nullable File oldVisibleFile, @Nullable File newVisibleFile) { if (oldVisibleFile != null) { - MediaStore.scanFile(getContext(), oldVisibleFile); + MediaStore.scanFile(getContext().getContentResolver(), oldVisibleFile); } if (newVisibleFile != null) { - MediaStore.scanFile(getContext(), newVisibleFile); + MediaStore.scanFile(getContext().getContentResolver(), newVisibleFile); } } diff --git a/media/java/android/media/MediaScannerConnection.java b/media/java/android/media/MediaScannerConnection.java index 515f6a8ce8a2..40e90731f2a2 100644 --- a/media/java/android/media/MediaScannerConnection.java +++ b/media/java/android/media/MediaScannerConnection.java @@ -19,6 +19,7 @@ package android.media; import android.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.ContentProviderClient; +import android.content.ContentResolver; import android.content.Context; import android.content.ServiceConnection; import android.net.Uri; @@ -197,7 +198,7 @@ public class MediaScannerConnection implements ServiceConnection { private static Uri scanFileQuietly(ContentProviderClient client, File file) { Uri uri = null; try { - uri = MediaStore.scanFile(client, file.getCanonicalFile()); + uri = MediaStore.scanFile(ContentResolver.wrap(client), file.getCanonicalFile()); Log.d(TAG, "Scanned " + file + " to " + uri); } catch (Exception e) { Log.w(TAG, "Failed to scan " + file + ": " + e); diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java index 9064e6891be6..a1e159174f95 100644 --- a/media/java/android/media/RingtoneManager.java +++ b/media/java/android/media/RingtoneManager.java @@ -908,7 +908,7 @@ public class RingtoneManager { } // Tell MediaScanner about the new file. Wait for it to assign a {@link Uri}. - return MediaStore.scanFile(mContext, outFile); + return MediaStore.scanFile(mContext.getContentResolver(), outFile); } private static final String getExternalDirectoryForType(final int type) { diff --git a/media/java/android/media/ThumbnailUtils.java b/media/java/android/media/ThumbnailUtils.java index a315c1eefb52..6705b0cb79f0 100644 --- a/media/java/android/media/ThumbnailUtils.java +++ b/media/java/android/media/ThumbnailUtils.java @@ -33,14 +33,13 @@ import android.graphics.ImageDecoder; import android.graphics.ImageDecoder.ImageInfo; import android.graphics.ImageDecoder.Source; import android.graphics.Matrix; -import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.CancellationSignal; import android.os.Environment; import android.os.ParcelFileDescriptor; -import android.provider.MediaStore.ThumbnailConstants; +import android.provider.MediaStore; import android.util.Log; import android.util.Size; @@ -77,15 +76,7 @@ public class ThumbnailUtils { public static final int OPTIONS_RECYCLE_INPUT = 0x2; private static Size convertKind(int kind) { - if (kind == ThumbnailConstants.MICRO_KIND) { - return Point.convert(ThumbnailConstants.MICRO_SIZE); - } else if (kind == ThumbnailConstants.FULL_SCREEN_KIND) { - return Point.convert(ThumbnailConstants.FULL_SCREEN_SIZE); - } else if (kind == ThumbnailConstants.MINI_KIND) { - return Point.convert(ThumbnailConstants.MINI_SIZE); - } else { - throw new IllegalArgumentException("Unsupported kind: " + kind); - } + return MediaStore.Images.Thumbnails.getKindSize(kind); } private static class Resizer implements ImageDecoder.OnHeaderDecodedListener { diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java index 0f402eb0bf42..f3c071a06eba 100755 --- a/media/java/android/mtp/MtpDatabase.java +++ b/media/java/android/mtp/MtpDatabase.java @@ -16,8 +16,10 @@ package android.mtp; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.ContentProviderClient; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -422,13 +424,13 @@ public class MtpDatabase implements AutoCloseable { } // Add the new file to MediaProvider if (succeeded) { - MediaStore.scanFile(mContext, obj.getPath().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), obj.getPath().toFile()); } } @VisibleForNative private void rescanFile(String path, int handle, int format) { - MediaStore.scanFile(mContext, new File(path)); + MediaStore.scanFile(mContext.getContentResolver(), new File(path)); } @VisibleForNative @@ -587,13 +589,13 @@ public class MtpDatabase implements AutoCloseable { if (obj.isDir()) { // for directories, check if renamed from something hidden to something non-hidden if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) { - MediaStore.scanFile(mContext, newPath.toFile()); + MediaStore.scanFile(mContext.getContentResolver(), newPath.toFile()); } } else { // for files, check if renamed from .nomedia to something else if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA) && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) { - MediaStore.scanFile(mContext, newPath.getParent().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), newPath.getParent().toFile()); } } return MtpConstants.RESPONSE_OK; @@ -662,7 +664,7 @@ public class MtpDatabase implements AutoCloseable { mMediaProvider.update(objectsUri, values, PATH_WHERE, whereArgs); } else { // Old parent doesn't exist - add the object - MediaStore.scanFile(mContext, path.toFile()); + MediaStore.scanFile(mContext.getContentResolver(), path.toFile()); } } catch (RemoteException e) { Log.e(TAG, "RemoteException in mMediaProvider.update", e); @@ -689,7 +691,7 @@ public class MtpDatabase implements AutoCloseable { if (!success) { return; } - MediaStore.scanFile(mContext, obj.getPath().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), obj.getPath().toFile()); } @VisibleForNative @@ -909,7 +911,7 @@ public class MtpDatabase implements AutoCloseable { String[] whereArgs = new String[]{path.toString()}; if (mMediaProvider.delete(objectsUri, PATH_WHERE, whereArgs) > 0) { if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) { - MediaStore.scanFile(mContext, path.getParent().toFile()); + MediaStore.scanFile(mContext.getContentResolver(), path.getParent().toFile()); } } else { Log.i(TAG, "Mediaprovider didn't delete " + path); diff --git a/media/java/android/mtp/MtpStorage.java b/media/java/android/mtp/MtpStorage.java index 65d0fef74b25..c7dbca61f90a 100644 --- a/media/java/android/mtp/MtpStorage.java +++ b/media/java/android/mtp/MtpStorage.java @@ -41,11 +41,7 @@ public class MtpStorage { mDescription = volume.getDescription(null); mRemovable = volume.isRemovable(); mMaxFileSize = volume.getMaxFileSize(); - if (volume.isPrimary()) { - mVolumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY; - } else { - mVolumeName = volume.getNormalizedUuid(); - } + mVolumeName = volume.getMediaStoreVolumeName(); } /** diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java b/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java index 2d37b4c9c342..15fd1f7f4957 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtoneOverlayService.java @@ -101,7 +101,7 @@ public class RingtoneOverlayService extends Service { } private Uri scanFile(@NonNull final File file) { - return MediaStore.scanFile(this, file); + return MediaStore.scanFile(getContentResolver(), file); } private void set(@NonNull final String name, @NonNull final Uri uri) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 0383dee4f9c3..d3ccbeb0afb1 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -371,7 +371,8 @@ public class RecordingService extends Service { Bitmap thumbnailBitmap = null; try { ContentResolver resolver = getContentResolver(); - Size size = Point.convert(MediaStore.ThumbnailConstants.MINI_SIZE); + DisplayMetrics metrics = getResources().getDisplayMetrics(); + Size size = new Size(metrics.widthPixels, metrics.heightPixels / 2); thumbnailBitmap = resolver.loadThumbnail(uri, size, null); } catch (IOException e) { Log.e(TAG, "Error creating thumbnail: " + e.getMessage()); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 76925b43cfb8..2f401e5271d8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -23,6 +23,8 @@ import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipDescription; import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; @@ -48,7 +50,9 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.DeviceConfig; import android.provider.MediaStore; +import android.provider.MediaStore.MediaColumns; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -275,6 +279,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); Context context = mParams.context; + ContentResolver resolver = context.getContentResolver(); Bitmap image = mParams.image; Resources r = context.getResources(); @@ -284,23 +289,27 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mSmartActionsProvider, mSmartActionsEnabled, isManagedProfile(context)); // Save the screenshot to the MediaStore - final MediaStore.PendingParams params = new MediaStore.PendingParams( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mImageFileName, "image/png"); - params.setRelativePath(Environment.DIRECTORY_PICTURES + File.separator - + Environment.DIRECTORY_SCREENSHOTS); - - final Uri uri = MediaStore.createPending(context, params); - final MediaStore.PendingSession session = MediaStore.openPending(context, uri); + final ContentValues values = new ContentValues(); + values.put(MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + + File.separator + Environment.DIRECTORY_SCREENSHOTS); + values.put(MediaColumns.DISPLAY_NAME, mImageFileName); + values.put(MediaColumns.MIME_TYPE, "image/png"); + values.put(MediaColumns.DATE_ADDED, mImageTime / 1000); + values.put(MediaColumns.DATE_MODIFIED, mImageTime / 1000); + values.put(MediaColumns.DATE_EXPIRES, (mImageTime + DateUtils.DAY_IN_MILLIS) / 1000); + values.put(MediaColumns.IS_PENDING, 1); + + final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); try { // First, write the actual data for our screenshot - try (OutputStream out = session.openOutputStream()) { + try (OutputStream out = resolver.openOutputStream(uri)) { if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) { throw new IOException("Failed to compress"); } } // Next, write metadata to help index the screenshot - try (ParcelFileDescriptor pfd = session.open()) { + try (ParcelFileDescriptor pfd = resolver.openFile(uri, "rw", null)) { final ExifInterface exif = new ExifInterface(pfd.getFileDescriptor()); exif.setAttribute(ExifInterface.TAG_SOFTWARE, @@ -327,12 +336,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { exif.saveAttributes(); } - session.publish(); + + // Everything went well above, publish it! + values.clear(); + values.put(MediaColumns.IS_PENDING, 0); + values.putNull(MediaColumns.DATE_EXPIRES); + resolver.update(uri, values, null, null); } catch (Exception e) { - session.abandon(); + resolver.delete(uri, null); throw e; - } finally { - IoUtils.closeQuietly(session); } populateNotificationActions(context, r, uri, smartActionsFuture, mNotificationBuilder); diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 9b1d9e9a246c..5a78036911a8 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -118,6 +118,7 @@ import android.sysprop.VoldProperties; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; +import android.util.ArraySet; import android.util.AtomicFile; import android.util.DataUnit; import android.util.FeatureFlagUtils; @@ -3161,16 +3162,20 @@ class StorageManagerService extends IStorageManager.Stub final boolean forWrite = (flags & StorageManager.FLAG_FOR_WRITE) != 0; final boolean realState = (flags & StorageManager.FLAG_REAL_STATE) != 0; final boolean includeInvisible = (flags & StorageManager.FLAG_INCLUDE_INVISIBLE) != 0; + final boolean includeRecent = (flags & StorageManager.FLAG_INCLUDE_RECENT) != 0; // Report all volumes as unmounted until we've recorded that user 0 has unlocked. There // are no guarantees that callers will see a consistent view of the volume before that // point final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM); + final boolean userIsDemo; final boolean userKeyUnlocked; final boolean storagePermission; final long token = Binder.clearCallingIdentity(); try { + userIsDemo = LocalServices.getService(UserManagerInternal.class) + .getUserInfo(userId).isDemo(); userKeyUnlocked = isUserKeyUnlocked(userId); storagePermission = mStorageManagerInternal.hasExternalStorage(uid, packageName); } finally { @@ -3180,6 +3185,7 @@ class StorageManagerService extends IStorageManager.Stub boolean foundPrimary = false; final ArrayList<StorageVolume> res = new ArrayList<>(); + final ArraySet<String> resUuids = new ArraySet<>(); synchronized (mLock) { for (int i = 0; i < mVolumes.size(); i++) { final VolumeInfo vol = mVolumes.valueAt(i); @@ -3222,7 +3228,43 @@ class StorageManagerService extends IStorageManager.Stub } else { res.add(userVol); } + resUuids.add(userVol.getUuid()); } + + if (includeRecent) { + final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS; + for (int i = 0; i < mRecords.size(); i++) { + final VolumeRecord rec = mRecords.valueAt(i); + + // Skip if we've already included it above + if (resUuids.contains(rec.fsUuid)) continue; + + // Treat as recent if mounted within the last week + if (rec.lastSeenMillis > 0 && rec.lastSeenMillis < lastWeek) { + final StorageVolume userVol = rec.buildStorageVolume(mContext); + res.add(userVol); + resUuids.add(userVol.getUuid()); + } + } + } + } + + // Synthesize a volume for preloaded media under demo users, so that + // it's scanned into MediaStore + if (userIsDemo) { + final String id = "demo"; + final File path = Environment.getDataPreloadsMediaDirectory(); + final boolean primary = false; + final boolean removable = false; + final boolean emulated = true; + final boolean allowMassStorage = false; + final long maxFileSize = 0; + final UserHandle user = new UserHandle(userId); + final String envState = Environment.MEDIA_MOUNTED_READ_ONLY; + final String description = mContext.getString(android.R.string.unknownName); + + res.add(new StorageVolume(id, path, path, description, primary, removable, + emulated, allowMassStorage, maxFileSize, user, id, envState)); } if (!foundPrimary) { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 1153fb521c53..99d5e4a21df4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -17962,14 +17962,6 @@ public class PackageManagerService extends IPackageManager.Stub } } mPermissionManager.resetRuntimePermissions(pkg, nextUserId); - // Also delete contributed media, when requested - if ((flags & PackageManager.DELETE_CONTRIBUTED_MEDIA) != 0) { - try { - MediaStore.deleteContributedMedia(mContext, ps.name, UserHandle.of(nextUserId)); - } catch (IOException e) { - Slog.w(TAG, "Failed to delete contributed media for " + ps.name, e); - } - } } if (outInfo != null) { |