Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 047edaea authored by Jeff Sharkey's avatar Jeff Sharkey Committed by Android (Google) Code Review
Browse files

Merge changes I86f597f1,I5c9c09dd

* changes:
  MediaStore deprecations for Q.
  Convenience method for obtaining thumbnails.
parents e428c326 5282796a
Loading
Loading
Loading
Loading
+14 −13
Original line number Diff line number Diff line
@@ -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
+67 −0
Original line number Diff line number Diff line
@@ -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);
            }
        });
    }
}
+10 −72
Original line number Diff line number Diff line
@@ -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);
    }

    /**
+172 −92

File changed.

Preview size limit exceeded, changes collapsed.

+142 −24
Original line number Diff line number Diff line
@@ -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;
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);

@Suppress  // Failing.
public class ContentResolverTest extends AndroidTestCase {
    private ContentResolver mContentResolver;
        // Size should be untouched
        assertEquals(1280, res.getWidth());
        assertEquals(960, res.getHeight());

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mContentResolver = mContext.getContentResolver();
        assertImageAspectAndContents(res);
    }

    @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);
    @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);
    }
}
Loading