diff options
| -rwxr-xr-x | api/current.txt | 27 | ||||
| -rw-r--r-- | core/java/android/content/ContentResolver.java | 67 | ||||
| -rw-r--r-- | core/java/android/provider/DocumentsContract.java | 82 | ||||
| -rw-r--r-- | core/java/android/provider/MediaStore.java | 264 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/content/ContentResolverTest.java | 166 | ||||
| -rw-r--r-- | graphics/java/android/graphics/Point.java | 11 |
6 files changed, 416 insertions, 201 deletions
diff --git a/api/current.txt b/api/current.txt index 49d63673de11..abba44742a11 100755 --- a/api/current.txt +++ b/api/current.txt @@ -9316,6 +9316,7 @@ package android.content { method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues); method public static boolean isSyncActive(android.accounts.Account, java.lang.String); method public static boolean isSyncPending(android.accounts.Account, java.lang.String); + method public android.graphics.Bitmap loadThumbnail(android.net.Uri, android.util.Size, android.os.CancellationSignal) throws java.io.IOException; method public void notifyChange(android.net.Uri, android.database.ContentObserver); method public void notifyChange(android.net.Uri, android.database.ContentObserver, boolean); method public void notifyChange(android.net.Uri, android.database.ContentObserver, int); @@ -36810,7 +36811,7 @@ package android.provider { public static abstract interface MediaStore.Audio.AlbumColumns { field public static final java.lang.String ALBUM = "album"; - field public static final java.lang.String ALBUM_ART = "album_art"; + field public static final deprecated java.lang.String ALBUM_ART = "album_art"; field public static final java.lang.String ALBUM_ID = "album_id"; field public static final java.lang.String ALBUM_KEY = "album_key"; field public static final java.lang.String ARTIST = "artist"; @@ -36932,7 +36933,7 @@ package android.provider { } public static abstract interface MediaStore.Audio.PlaylistsColumns { - field public static final java.lang.String DATA = "_data"; + field public static final deprecated java.lang.String DATA = "_data"; field public static final java.lang.String DATE_ADDED = "date_added"; field public static final java.lang.String DATE_MODIFIED = "date_modified"; field public static final java.lang.String NAME = "name"; @@ -36994,15 +36995,15 @@ package android.provider { public static class MediaStore.Images.Thumbnails implements android.provider.BaseColumns { ctor public MediaStore.Images.Thumbnails(); - method public static void cancelThumbnailRequest(android.content.ContentResolver, long); - method public static void cancelThumbnailRequest(android.content.ContentResolver, long, long); + method public static deprecated void cancelThumbnailRequest(android.content.ContentResolver, long); + method public static deprecated void cancelThumbnailRequest(android.content.ContentResolver, long, long); method public static android.net.Uri getContentUri(java.lang.String); - method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); - method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); + method public static deprecated android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); + method public static deprecated android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); method public static final android.database.Cursor query(android.content.ContentResolver, android.net.Uri, java.lang.String[]); method public static final android.database.Cursor queryMiniThumbnail(android.content.ContentResolver, long, int, java.lang.String[]); method public static final android.database.Cursor queryMiniThumbnails(android.content.ContentResolver, android.net.Uri, int, java.lang.String[]); - field public static final java.lang.String DATA = "_data"; + field public static final deprecated java.lang.String DATA = "_data"; field public static final java.lang.String DEFAULT_SORT_ORDER = "image_id ASC"; field public static final android.net.Uri EXTERNAL_CONTENT_URI; field public static final int FULL_SCREEN_KIND = 2; // 0x2 @@ -37017,7 +37018,7 @@ package android.provider { } public static abstract interface MediaStore.MediaColumns implements android.provider.BaseColumns { - field public static final java.lang.String DATA = "_data"; + field public static final deprecated java.lang.String DATA = "_data"; field public static final java.lang.String DATE_ADDED = "date_added"; field public static final java.lang.String DATE_MODIFIED = "date_modified"; field public static final java.lang.String DISPLAY_NAME = "_display_name"; @@ -37045,12 +37046,12 @@ package android.provider { public static class MediaStore.Video.Thumbnails implements android.provider.BaseColumns { ctor public MediaStore.Video.Thumbnails(); - method public static void cancelThumbnailRequest(android.content.ContentResolver, long); - method public static void cancelThumbnailRequest(android.content.ContentResolver, long, long); + method public static deprecated void cancelThumbnailRequest(android.content.ContentResolver, long); + method public static deprecated void cancelThumbnailRequest(android.content.ContentResolver, long, long); method public static android.net.Uri getContentUri(java.lang.String); - method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); - method public static android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); - field public static final java.lang.String DATA = "_data"; + method public static deprecated android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, int, android.graphics.BitmapFactory.Options); + method public static deprecated android.graphics.Bitmap getThumbnail(android.content.ContentResolver, long, long, int, android.graphics.BitmapFactory.Options); + field public static final deprecated java.lang.String DATA = "_data"; field public static final java.lang.String DEFAULT_SORT_ORDER = "video_id ASC"; field public static final android.net.Uri EXTERNAL_CONTENT_URI; field public static final int FULL_SCREEN_KIND = 2; // 0x2 diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index c19909da3007..599c2d2d3594 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -35,6 +35,12 @@ import android.database.ContentObserver; import android.database.CrossProcessCursorWrapper; import android.database.Cursor; import android.database.IContentObserver; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ImageDecoder; +import android.graphics.ImageDecoder.ImageInfo; +import android.graphics.ImageDecoder.Source; +import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -49,9 +55,11 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; +import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; +import android.util.Size; import com.android.internal.util.MimeIconUtils; import com.android.internal.util.Preconditions; @@ -68,6 +76,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Random; import java.util.concurrent.atomic.AtomicBoolean; @@ -3188,4 +3197,62 @@ public abstract class ContentResolver { } return query; } + + /** + * Convenience method that efficiently loads a visual thumbnail for the + * given {@link Uri}. Internally calls + * {@link ContentProvider#openTypedAssetFile} on the remote provider, but + * also defensively resizes any returned content to match the requested + * target size. + * + * @param uri The item that should be visualized as a thumbnail. + * @param size The target area on the screen where this thumbnail will be + * shown. This is passed to the provider as {@link #EXTRA_SIZE} + * to help it avoid downloading or generating heavy resources. + * @param signal A signal to cancel the operation in progress. + * @return Valid {@link Bitmap} which is a visual thumbnail. + * @throws IOException If any trouble was encountered while generating or + * loading the thumbnail, or if + * {@link CancellationSignal#cancel()} was invoked. + */ + public @NonNull Bitmap loadThumbnail(@NonNull Uri uri, @NonNull Size size, + @Nullable CancellationSignal signal) throws IOException { + Objects.requireNonNull(uri); + Objects.requireNonNull(size); + + try (ContentProviderClient client = acquireContentProviderClient(uri)) { + return loadThumbnail(client, uri, size, signal, ImageDecoder.ALLOCATOR_DEFAULT); + } + } + + /** {@hide} */ + public static Bitmap loadThumbnail(@NonNull ContentProviderClient client, @NonNull Uri uri, + @NonNull Size size, @Nullable CancellationSignal signal, int allocator) + throws IOException { + Objects.requireNonNull(client); + Objects.requireNonNull(uri); + Objects.requireNonNull(size); + + // Convert to Point, since that's what the API is defined as + final Bundle opts = new Bundle(); + opts.putParcelable(EXTRA_SIZE, Point.convert(size)); + + return ImageDecoder.decodeBitmap(ImageDecoder.createSource(() -> { + return client.openTypedAssetFileDescriptor(uri, "image/*", opts, signal); + }), (ImageDecoder decoder, ImageInfo info, Source source) -> { + decoder.setAllocator(allocator); + + // One last-ditch check to see if we've been canceled. + if (signal != null) signal.throwIfCanceled(); + + // We requested a rough thumbnail size, but the remote size may have + // returned something giant, so defensively scale down as needed. + final int widthSample = info.getSize().getWidth() / size.getWidth(); + final int heightSample = info.getSize().getHeight() / size.getHeight(); + final int sample = Math.min(widthSample, heightSample); + if (sample > 1) { + decoder.setTargetSampleSize(sample); + } + }); + } } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 8c40e0e6cb8c..954d18abc6e1 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -34,6 +34,7 @@ import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.Point; import android.media.ExifInterface; @@ -53,6 +54,7 @@ import android.system.ErrnoException; import android.system.Os; import android.util.DataUnit; import android.util.Log; +import android.util.Size; import libcore.io.IoUtils; @@ -136,10 +138,11 @@ public final class DocumentsContract { public static final String EXTRA_EXCLUDE_SELF = "android.provider.extra.EXCLUDE_SELF"; /** - * Included in {@link AssetFileDescriptor#getExtras()} when returned - * thumbnail should be rotated. + * An extra number of degrees that an image should be rotated during the + * decode process to be presented correctly. * - * @see MediaStore.Images.ImageColumns#ORIENTATION + * @see AssetFileDescriptor#getExtras() + * @see android.provider.MediaStore.Images.ImageColumns#ORIENTATION */ public static final String EXTRA_ORIENTATION = "android.provider.extra.ORIENTATION"; @@ -1093,75 +1096,10 @@ public final class DocumentsContract { /** {@hide} */ @UnsupportedAppUsage - public static Bitmap getDocumentThumbnail( - ContentProviderClient client, Uri documentUri, Point size, CancellationSignal signal) - throws RemoteException, IOException { - final Bundle openOpts = new Bundle(); - openOpts.putParcelable(ContentResolver.EXTRA_SIZE, size); - - AssetFileDescriptor afd = null; - Bitmap bitmap = null; - try { - afd = client.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts, signal); - - final FileDescriptor fd = afd.getFileDescriptor(); - final long offset = afd.getStartOffset(); - - // Try seeking on the returned FD, since it gives us the most - // optimal decode path; otherwise fall back to buffering. - BufferedInputStream is = null; - try { - Os.lseek(fd, offset, SEEK_SET); - } catch (ErrnoException e) { - is = new BufferedInputStream(new FileInputStream(fd), THUMBNAIL_BUFFER_SIZE); - is.mark(THUMBNAIL_BUFFER_SIZE); - } - - // We requested a rough thumbnail size, but the remote size may have - // returned something giant, so defensively scale down as needed. - final BitmapFactory.Options opts = new BitmapFactory.Options(); - opts.inJustDecodeBounds = true; - if (is != null) { - BitmapFactory.decodeStream(is, null, opts); - } else { - BitmapFactory.decodeFileDescriptor(fd, null, opts); - } - - final int widthSample = opts.outWidth / size.x; - final int heightSample = opts.outHeight / size.y; - - opts.inJustDecodeBounds = false; - opts.inSampleSize = Math.min(widthSample, heightSample); - if (is != null) { - is.reset(); - bitmap = BitmapFactory.decodeStream(is, null, opts); - } else { - try { - Os.lseek(fd, offset, SEEK_SET); - } catch (ErrnoException e) { - e.rethrowAsIOException(); - } - bitmap = BitmapFactory.decodeFileDescriptor(fd, null, opts); - } - - // Transform the bitmap if requested. We use a side-channel to - // communicate the orientation, since EXIF thumbnails don't contain - // the rotation flags of the original image. - final Bundle extras = afd.getExtras(); - final int orientation = (extras != null) ? extras.getInt(EXTRA_ORIENTATION, 0) : 0; - if (orientation != 0) { - final int width = bitmap.getWidth(); - final int height = bitmap.getHeight(); - - final Matrix m = new Matrix(); - m.setRotate(orientation, width / 2, height / 2); - bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, m, false); - } - } finally { - IoUtils.closeQuietly(afd); - } - - return bitmap; + public static Bitmap getDocumentThumbnail(ContentProviderClient client, Uri documentUri, + Point size, CancellationSignal signal) throws IOException { + return ContentResolver.loadThumbnail(client, documentUri, Point.convert(size), signal, + ImageDecoder.ALLOCATOR_DEFAULT); } /** diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 828fd7386d80..f5660b950c0a 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -29,7 +29,6 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.UriPermission; -import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.DatabaseUtils; import android.graphics.Bitmap; @@ -39,6 +38,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Environment; +import android.os.OperationCanceledException; import android.os.RemoteException; import android.service.media.CameraPrewarmService; import android.util.ArrayMap; @@ -402,7 +402,16 @@ public final class MediaStore { * access. * <p> * Type: TEXT + * + * @deprecated Apps may not have filesystem permissions to directly + * access this path. Instead of trying to open this path + * directly, apps should use + * {@link ContentResolver#openFileDescriptor(Uri, String)} + * to gain access. This value will always be {@code NULL} + * for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} or higher. */ + @Deprecated public static final String DATA = "_data"; /** @@ -641,6 +650,7 @@ public final class MediaStore { * This class is used internally by Images.Thumbnails and Video.Thumbnails, it's not intended * to be accessed elsewhere. */ + @Deprecated private static class InternalThumbnails implements BaseColumns { /** * Currently outstanding thumbnail requests that can be cancelled. @@ -654,13 +664,14 @@ public final class MediaStore { * * @see #cancelThumbnail(ContentResolver, Uri) */ + @Deprecated static @Nullable Bitmap getThumbnail(@NonNull ContentResolver cr, @NonNull Uri uri, int kind, @Nullable BitmapFactory.Options opts) { - final Bundle openOpts = new Bundle(); + final Point size; if (kind == ThumbnailConstants.MICRO_KIND) { - openOpts.putParcelable(ContentResolver.EXTRA_SIZE, ThumbnailConstants.MICRO_SIZE); + size = ThumbnailConstants.MICRO_SIZE; } else if (kind == ThumbnailConstants.MINI_KIND) { - openOpts.putParcelable(ContentResolver.EXTRA_SIZE, ThumbnailConstants.MINI_SIZE); + size = ThumbnailConstants.MINI_SIZE; } else { throw new IllegalArgumentException("Unsupported kind: " + kind); } @@ -674,9 +685,8 @@ public final class MediaStore { } } - try (AssetFileDescriptor afd = cr.openTypedAssetFileDescriptor(uri, - "image/*", openOpts, signal)) { - return BitmapFactory.decodeFileDescriptor(afd.getFileDescriptor(), null, opts); + try { + return cr.loadThumbnail(uri, Point.convert(size), signal); } catch (IOException e) { Log.w(TAG, "Failed to obtain thumbnail for " + uri, e); return null; @@ -693,6 +703,7 @@ public final class MediaStore { * Only the original process which made the request can cancel their own * requests. */ + @Deprecated static void cancelThumbnail(@NonNull ContentResolver cr, @NonNull Uri uri) { synchronized (sPending) { final CancellationSignal signal = sPending.get(uri); @@ -936,9 +947,8 @@ public final class MediaStore { } /** - * This class allows developers to query and get two kinds of thumbnails: - * MINI_KIND: 512 x 384 thumbnail - * MICRO_KIND: 96 x 96 thumbnail + * This class provides utility methods to obtain thumbnails for various + * {@link Images} items. */ public static class Thumbnails implements BaseColumns { public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) { @@ -958,13 +968,19 @@ public final class MediaStore { } /** - * This method cancels the thumbnail request so clients waiting for getThumbnail will be - * interrupted and return immediately. Only the original process which made the getThumbnail - * requests can cancel their own requests. + * Cancel any outstanding {@link #getThumbnail} requests, causing + * them to return by throwing a {@link OperationCanceledException}. + * <p> + * This method has no effect on + * {@link ContentResolver#loadThumbnail} calls, since they provide + * their own {@link CancellationSignal}. * - * @param cr ContentResolver - * @param origId original image id + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. */ + @Deprecated public static void cancelThumbnailRequest(ContentResolver cr, long origId) { final Uri uri = ContentUris.withAppendedId( Images.Media.EXTERNAL_CONTENT_URI, origId); @@ -972,51 +988,66 @@ public final class MediaStore { } /** - * This method checks if the thumbnails of the specified image (origId) has been created. - * It will be blocked until the thumbnails are generated. + * Return thumbnail representing a specific image item. If a + * thumbnail doesn't exist, this method will block until it's + * generated. Callers are responsible for their own in-memory + * caching of returned values. * - * @param cr ContentResolver used to dispatch queries to MediaProvider. - * @param origId Original image id associated with thumbnail of interest. - * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. - * @param options this is only used for MINI_KIND when decoding the Bitmap - * @return A Bitmap instance. It could be null if the original image - * associated with origId doesn't exist or memory is not enough. - */ - public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + * @param imageId the image item to obtain a thumbnail for. + * @param kind optimal thumbnail size desired. + * @return decoded thumbnail, or {@code null} if problem was + * encountered. + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. + */ + @Deprecated + public static Bitmap getThumbnail(ContentResolver cr, long imageId, int kind, BitmapFactory.Options options) { final Uri uri = ContentUris.withAppendedId( - Images.Media.EXTERNAL_CONTENT_URI, origId); + Images.Media.EXTERNAL_CONTENT_URI, imageId); return InternalThumbnails.getThumbnail(cr, uri, kind, options); } /** - * This method cancels the thumbnail request so clients waiting for getThumbnail will be - * interrupted and return immediately. Only the original process which made the getThumbnail - * requests can cancel their own requests. + * Cancel any outstanding {@link #getThumbnail} requests, causing + * them to return by throwing a {@link OperationCanceledException}. + * <p> + * This method has no effect on + * {@link ContentResolver#loadThumbnail} calls, since they provide + * their own {@link CancellationSignal}. * - * @param cr ContentResolver - * @param origId original image id - * @param groupId the same groupId used in getThumbnail. + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. */ - public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) { + @Deprecated + public static void cancelThumbnailRequest(ContentResolver cr, long origId, + long groupId) { cancelThumbnailRequest(cr, origId); } /** - * This method checks if the thumbnails of the specified image (origId) has been created. - * It will be blocked until the thumbnails are generated. + * Return thumbnail representing a specific image item. If a + * thumbnail doesn't exist, this method will block until it's + * generated. Callers are responsible for their own in-memory + * caching of returned values. * - * @param cr ContentResolver used to dispatch queries to MediaProvider. - * @param origId Original image id associated with thumbnail of interest. - * @param groupId the id of group to which this request belongs - * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. - * @param options this is only used for MINI_KIND when decoding the Bitmap - * @return A Bitmap instance. It could be null if the original image - * associated with origId doesn't exist or memory is not enough. - */ - public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, + * @param imageId the image item to obtain a thumbnail for. + * @param kind optimal thumbnail size desired. + * @return decoded thumbnail, or {@code null} if problem was + * encountered. + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. + */ + @Deprecated + public static Bitmap getThumbnail(ContentResolver cr, long imageId, long groupId, int kind, BitmapFactory.Options options) { - return getThumbnail(cr, origId, kind, options); + return getThumbnail(cr, imageId, kind, options); } /** @@ -1059,7 +1090,16 @@ public final class MediaStore { * access. * <p> * Type: TEXT + * + * @deprecated Apps may not have filesystem permissions to directly + * access this path. Instead of trying to open this path + * directly, apps should use + * {@link ContentResolver#loadThumbnail} + * to gain access. This value will always be + * {@code NULL} for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} or higher. */ + @Deprecated public static final String DATA = "_data"; /** @@ -1509,7 +1549,16 @@ public final class MediaStore { * access. * <p> * Type: TEXT + * + * @deprecated Apps may not have filesystem permissions to directly + * access this path. Instead of trying to open this path + * directly, apps should use + * {@link ContentResolver#openFileDescriptor(Uri, String)} + * to gain access. This value will always be + * {@code NULL} for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} or higher. */ + @Deprecated public static final String DATA = "_data"; /** @@ -1790,7 +1839,16 @@ public final class MediaStore { /** * Cached album art. * <P>Type: TEXT</P> + * + * @deprecated Apps may not have filesystem permissions to directly + * access this path. Instead of trying to open this path + * directly, apps should use + * {@link ContentResolver#loadThumbnail} + * to gain access. This value will always be + * {@code NULL} for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} or higher. */ + @Deprecated public static final String ALBUM_ART = "album_art"; } @@ -2009,20 +2067,24 @@ public final class MediaStore { } /** - * This class allows developers to query and get two kinds of thumbnails: - * MINI_KIND: 512 x 384 thumbnail - * MICRO_KIND: 96 x 96 thumbnail - * + * This class provides utility methods to obtain thumbnails for various + * {@link Video} items. */ public static class Thumbnails implements BaseColumns { /** - * This method cancels the thumbnail request so clients waiting for getThumbnail will be - * interrupted and return immediately. Only the original process which made the getThumbnail - * requests can cancel their own requests. + * Cancel any outstanding {@link #getThumbnail} requests, causing + * them to return by throwing a {@link OperationCanceledException}. + * <p> + * This method has no effect on + * {@link ContentResolver#loadThumbnail} calls, since they provide + * their own {@link CancellationSignal}. * - * @param cr ContentResolver - * @param origId original video id + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. */ + @Deprecated public static void cancelThumbnailRequest(ContentResolver cr, long origId) { final Uri uri = ContentUris.withAppendedId( Video.Media.EXTERNAL_CONTENT_URI, origId); @@ -2030,51 +2092,66 @@ public final class MediaStore { } /** - * This method checks if the thumbnails of the specified image (origId) has been created. - * It will be blocked until the thumbnails are generated. + * Return thumbnail representing a specific video item. If a + * thumbnail doesn't exist, this method will block until it's + * generated. Callers are responsible for their own in-memory + * caching of returned values. * - * @param cr ContentResolver used to dispatch queries to MediaProvider. - * @param origId Original image id associated with thumbnail of interest. - * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. - * @param options this is only used for MINI_KIND when decoding the Bitmap - * @return A Bitmap instance. It could be null if the original image - * associated with origId doesn't exist or memory is not enough. - */ - public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + * @param videoId the video item to obtain a thumbnail for. + * @param kind optimal thumbnail size desired. + * @return decoded thumbnail, or {@code null} if problem was + * encountered. + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. + */ + @Deprecated + public static Bitmap getThumbnail(ContentResolver cr, long videoId, int kind, BitmapFactory.Options options) { final Uri uri = ContentUris.withAppendedId( - Video.Media.EXTERNAL_CONTENT_URI, origId); + Video.Media.EXTERNAL_CONTENT_URI, videoId); return InternalThumbnails.getThumbnail(cr, uri, kind, options); } /** - * This method checks if the thumbnails of the specified image (origId) has been created. - * It will be blocked until the thumbnails are generated. + * Cancel any outstanding {@link #getThumbnail} requests, causing + * them to return by throwing a {@link OperationCanceledException}. + * <p> + * This method has no effect on + * {@link ContentResolver#loadThumbnail} calls, since they provide + * their own {@link CancellationSignal}. * - * @param cr ContentResolver used to dispatch queries to MediaProvider. - * @param origId Original image id associated with thumbnail of interest. - * @param groupId the id of group to which this request belongs - * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND - * @param options this is only used for MINI_KIND when decoding the Bitmap - * @return A Bitmap instance. It could be null if the original image associated with - * origId doesn't exist or memory is not enough. - */ - public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, - int kind, BitmapFactory.Options options) { - return getThumbnail(cr, origId, kind, options); + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. + */ + @Deprecated + public static void cancelThumbnailRequest(ContentResolver cr, long videoId, + long groupId) { + cancelThumbnailRequest(cr, videoId); } /** - * This method cancels the thumbnail request so clients waiting for getThumbnail will be - * interrupted and return immediately. Only the original process which made the getThumbnail - * requests can cancel their own requests. + * Return thumbnail representing a specific video item. If a + * thumbnail doesn't exist, this method will block until it's + * generated. Callers are responsible for their own in-memory + * caching of returned values. * - * @param cr ContentResolver - * @param origId original video id - * @param groupId the same groupId used in getThumbnail. + * @param videoId the video item to obtain a thumbnail for. + * @param kind optimal thumbnail size desired. + * @return decoded thumbnail, or {@code null} if problem was + * encountered. + * @deprecated Callers should migrate to using + * {@link ContentResolver#loadThumbnail}, since it + * offers richer control over requested thumbnail sizes + * and cancellation behavior. */ - public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) { - cancelThumbnailRequest(cr, origId); + @Deprecated + public static Bitmap getThumbnail(ContentResolver cr, long videoId, long groupId, + int kind, BitmapFactory.Options options) { + return getThumbnail(cr, videoId, kind, options); } /** @@ -2110,14 +2187,17 @@ public final class MediaStore { /** * Path to the thumbnail file on disk. * <p> - * Note that apps may not have filesystem permissions to directly - * access this path. Instead of trying to open this path directly, - * apps should use - * {@link ContentResolver#openFileDescriptor(Uri, String)} to gain - * access. - * <p> * Type: TEXT + * + * @deprecated Apps may not have filesystem permissions to directly + * access this path. Instead of trying to open this path + * directly, apps should use + * {@link ContentResolver#openFileDescriptor(Uri, String)} + * to gain access. This value will always be + * {@code NULL} for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} or higher. */ + @Deprecated public static final String DATA = "_data"; /** diff --git a/core/tests/coretests/src/android/content/ContentResolverTest.java b/core/tests/coretests/src/android/content/ContentResolverTest.java index 6256d08adb00..0036186994fe 100644 --- a/core/tests/coretests/src/android/content/ContentResolverTest.java +++ b/core/tests/coretests/src/android/content/ContentResolverTest.java @@ -13,31 +13,149 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package android.content; -import android.content.ContentResolver; -import android.provider.ContactsContract; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.LargeTest; -import android.test.suitebuilder.annotation.Suppress; - -@Suppress // Failing. -public class ContentResolverTest extends AndroidTestCase { - private ContentResolver mContentResolver; - - @Override - protected void setUp() throws Exception { - super.setUp(); - mContentResolver = mContext.getContentResolver(); - } - - @LargeTest - public void testCursorFinalizer() throws Exception { - // TODO: Want a test case that more predictably reproduce this issue. Selected - // 600 as this causes the problem 100% of the runs on current hw, it might not - // do so on some other configuration though. - for (int i = 0; i < 600; i++) { - mContentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null); - } +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ImageDecoder; +import android.graphics.Paint; +import android.net.Uri; +import android.os.MemoryFile; +import android.os.ParcelFileDescriptor; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.util.Size; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class ContentResolverTest { + + private ContentResolver mResolver; + private IContentProvider mProvider; + private ContentProviderClient mClient; + + private int mSize = 256_000; + private MemoryFile mImage; + + @Before + public void setUp() throws Exception { + mResolver = InstrumentationRegistry.getInstrumentation().getTargetContext() + .getContentResolver(); + mProvider = mock(IContentProvider.class); + mClient = new ContentProviderClient(mResolver, mProvider, false); + + mImage = new MemoryFile("temp.png", mSize); + } + + @After + public void tearDown() throws Exception { + mImage.close(); + mImage = null; + } + + private void initImage(int width, int height) throws Exception { + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + canvas.drawColor(Color.RED); + + final Paint paint = new Paint(); + paint.setColor(Color.BLUE); + paint.setStyle(Paint.Style.FILL); + canvas.drawRect(0, 0, width / 2, height / 2, paint); + + bitmap.compress(Bitmap.CompressFormat.PNG, 90, mImage.getOutputStream()); + + final AssetFileDescriptor afd = new AssetFileDescriptor( + new ParcelFileDescriptor(mImage.getFileDescriptor()), 0, mSize, null); + when(mProvider.openTypedAssetFile(any(), any(), any(), any(), any())).thenReturn(afd); + } + + private static void assertImageAspectAndContents(Bitmap bitmap) { + // And correct aspect ratio + final int before = (100 * 1280) / 960; + final int after = (100 * bitmap.getWidth()) / bitmap.getHeight(); + assertEquals(before, after); + + // And scaled correctly + final int halfX = bitmap.getWidth() / 2; + final int halfY = bitmap.getHeight() / 2; + assertEquals(Color.BLUE, bitmap.getPixel(halfX - 10, halfY - 10)); + assertEquals(Color.RED, bitmap.getPixel(halfX + 10, halfY - 10)); + assertEquals(Color.RED, bitmap.getPixel(halfX - 10, halfY + 10)); + assertEquals(Color.RED, bitmap.getPixel(halfX + 10, halfY + 10)); + } + + @Test + public void testLoadThumbnail_Normal() throws Exception { + initImage(1280, 960); + + Bitmap res = ContentResolver.loadThumbnail(mClient, + Uri.parse("content://com.example/"), new Size(1280, 960), null, + ImageDecoder.ALLOCATOR_SOFTWARE); + + // Size should be untouched + assertEquals(1280, res.getWidth()); + assertEquals(960, res.getHeight()); + + assertImageAspectAndContents(res); + } + + @Test + public void testLoadThumbnail_Scaling() throws Exception { + initImage(1280, 960); + + Bitmap res = ContentResolver.loadThumbnail(mClient, + Uri.parse("content://com.example/"), new Size(320, 240), null, + ImageDecoder.ALLOCATOR_SOFTWARE); + + // Size should be much smaller + assertTrue(res.getWidth() <= 640); + assertTrue(res.getHeight() <= 480); + + assertImageAspectAndContents(res); + } + + @Test + public void testLoadThumbnail_Aspect() throws Exception { + initImage(1280, 960); + + Bitmap res = ContentResolver.loadThumbnail(mClient, + Uri.parse("content://com.example/"), new Size(240, 320), null, + ImageDecoder.ALLOCATOR_SOFTWARE); + + // Size should be much smaller + assertTrue(res.getWidth() <= 640); + assertTrue(res.getHeight() <= 480); + + assertImageAspectAndContents(res); + } + + @Test + public void testLoadThumbnail_Tiny() throws Exception { + initImage(32, 24); + + Bitmap res = ContentResolver.loadThumbnail(mClient, + Uri.parse("content://com.example/"), new Size(320, 240), null, + ImageDecoder.ALLOCATOR_SOFTWARE); + + // Size should be untouched + assertEquals(32, res.getWidth()); + assertEquals(24, res.getHeight()); + + assertImageAspectAndContents(res); } } diff --git a/graphics/java/android/graphics/Point.java b/graphics/java/android/graphics/Point.java index f291e2702948..ea9350174f97 100644 --- a/graphics/java/android/graphics/Point.java +++ b/graphics/java/android/graphics/Point.java @@ -19,6 +19,7 @@ package android.graphics; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; +import android.util.Size; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; @@ -168,4 +169,14 @@ public class Point implements Parcelable { x = in.readInt(); y = in.readInt(); } + + /** {@hide} */ + public static @NonNull Point convert(@NonNull Size size) { + return new Point(size.getWidth(), size.getHeight()); + } + + /** {@hide} */ + public static @NonNull Size convert(@NonNull Point point) { + return new Size(point.x, point.y); + } } |