blob: f0308d2fa4beaf31da99b8770919b2cd2cc49206 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.provider.cts;
import static android.provider.cts.MediaStoreTest.TAG;
import static android.provider.cts.ProviderTestUtils.assertExists;
import static android.provider.cts.ProviderTestUtils.assertNotExists;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageDecoder;
import android.net.Uri;
import android.os.Environment;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.provider.MediaStore;
import android.provider.MediaStore.Images.Media;
import android.provider.MediaStore.Images.Thumbnails;
import android.provider.MediaStore.MediaColumns;
import android.provider.cts.MediaStoreUtils.PendingParams;
import android.provider.cts.MediaStoreUtils.PendingSession;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import androidx.test.InstrumentationRegistry;
import junit.framework.AssertionFailedError;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.util.ArrayList;
@Presubmit
@RunWith(Parameterized.class)
public class MediaStore_Images_ThumbnailsTest {
private ArrayList<Uri> mRowsAdded;
private Context mContext;
private ContentResolver mContentResolver;
private Uri mExternalImages;
@Parameter(0)
public String mVolumeName;
private int mLargestDimension;
@Parameters
public static Iterable<? extends Object> data() {
return ProviderTestUtils.getSharedVolumeNames();
}
private Uri mRed;
private Uri mBlue;
@After
public void tearDown() throws Exception {
for (Uri row : mRowsAdded) {
try {
mContentResolver.delete(row, null, null);
} catch (UnsupportedOperationException e) {
// There is no way to delete rows from table "thumbnails" of internals database.
// ignores the exception and make the loop goes on
}
}
}
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getTargetContext();
mContentResolver = mContext.getContentResolver();
mRowsAdded = new ArrayList<Uri>();
Log.d(TAG, "Using volume " + mVolumeName);
mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName);
final Resources res = mContext.getResources();
final Configuration config = res.getConfiguration();
mLargestDimension = (int) (Math.max(config.screenWidthDp, config.screenHeightDp)
* res.getDisplayMetrics().density);
}
private void prepareImages() throws Exception {
mRed = ProviderTestUtils.stageMedia(R.raw.scenery, mExternalImages);
mBlue = ProviderTestUtils.stageMedia(R.raw.scenery, mExternalImages);
mRowsAdded.add(mRed);
mRowsAdded.add(mBlue);
}
public static void assertMostlyEquals(long expected, long actual, long delta) {
if (Math.abs(expected - actual) > delta) {
throw new AssertionFailedError("Expected roughly " + expected + " but was " + actual);
}
}
@Test
public void testQueryExternalThumbnails() throws Exception {
if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return;
prepareImages();
Cursor c = Thumbnails.queryMiniThumbnails(mContentResolver,
Thumbnails.EXTERNAL_CONTENT_URI, Thumbnails.MICRO_KIND, null);
int previousMicroKindCount = c.getCount();
c.close();
// add a thumbnail
final File file = new File(ProviderTestUtils.stageDir(MediaStore.VOLUME_EXTERNAL),
"testThumbnails.jpg");
final String path = file.getAbsolutePath();
ProviderTestUtils.stageFile(R.raw.scenery, file);
ContentValues values = new ContentValues();
values.put(Thumbnails.KIND, Thumbnails.MINI_KIND);
values.put(Thumbnails.DATA, path);
values.put(Thumbnails.IMAGE_ID, ContentUris.parseId(mRed));
Uri uri = mContentResolver.insert(Thumbnails.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
mRowsAdded.add(uri);
}
// query with the uri of the thumbnail and the kind
c = Thumbnails.queryMiniThumbnails(mContentResolver, uri, Thumbnails.MINI_KIND, null);
c.moveToFirst();
assertEquals(1, c.getCount());
assertEquals(Thumbnails.MINI_KIND, c.getInt(c.getColumnIndex(Thumbnails.KIND)));
assertEquals(path, c.getString(c.getColumnIndex(Thumbnails.DATA)));
// query all thumbnails with other kind
c = Thumbnails.queryMiniThumbnails(mContentResolver, Thumbnails.EXTERNAL_CONTENT_URI,
Thumbnails.MICRO_KIND, null);
assertEquals(previousMicroKindCount, c.getCount());
c.close();
// query without kind
c = Thumbnails.query(mContentResolver, uri, null);
assertEquals(1, c.getCount());
c.moveToFirst();
assertEquals(Thumbnails.MINI_KIND, c.getInt(c.getColumnIndex(Thumbnails.KIND)));
assertEquals(path, c.getString(c.getColumnIndex(Thumbnails.DATA)));
c.close();
}
@Test
public void testQueryExternalMiniThumbnails() throws Exception {
if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return;
final ContentResolver resolver = mContentResolver;
// insert the image by bitmap
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inTargetDensity = DisplayMetrics.DENSITY_XHIGH;
Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery,opts);
String stringUrl = null;
try{
stringUrl = Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), null);
} catch (UnsupportedOperationException e) {
// the tests will be aborted because the image will be put in sdcard
fail("There is no sdcard attached! " + e.getMessage());
}
assertNotNull(stringUrl);
Uri stringUri = Uri.parse(stringUrl);
mRowsAdded.add(stringUri);
// get the original image id and path
Cursor c = mContentResolver.query(stringUri,
new String[]{ Media._ID, Media.DATA }, null, null, null);
c.moveToFirst();
long imageId = c.getLong(c.getColumnIndex(Media._ID));
String imagePath = c.getString(c.getColumnIndex(Media.DATA));
c.close();
assertExists("image file does not exist", imagePath);
assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null));
assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null));
// deleting the image from the database also deletes the image file, and the
// corresponding entry in the thumbnail table, which in turn triggers deletion
// of the thumbnail file on disk
mContentResolver.delete(stringUri, null, null);
mRowsAdded.remove(stringUri);
assertNotExists("image file should no longer exist", imagePath);
assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null));
assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null));
// insert image, then delete it via the files table
stringUrl = Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), null);
c = mContentResolver.query(Uri.parse(stringUrl),
new String[]{ Media._ID, Media.DATA}, null, null, null);
c.moveToFirst();
imageId = c.getLong(c.getColumnIndex(Media._ID));
imagePath = c.getString(c.getColumnIndex(Media.DATA));
c.close();
assertExists("image file does not exist", imagePath);
Uri fileuri = MediaStore.Files.getContentUri("external", imageId);
mContentResolver.delete(fileuri, null, null);
assertNotExists("image file should no longer exist", imagePath);
}
@Test
public void testGetContentUri() {
Cursor c = null;
assertNotNull(c = mContentResolver.query(Thumbnails.getContentUri("internal"), null, null,
null, null));
c.close();
assertNotNull(c = mContentResolver.query(Thumbnails.getContentUri(mVolumeName), null, null,
null, null));
c.close();
}
@Test
public void testStoreImagesMediaExternal() throws Exception {
if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return;
prepareImages();
final String externalImgPath = Environment.getExternalStorageDirectory() +
"/testimage.jpg";
final String externalImgPath2 = Environment.getExternalStorageDirectory() +
"/testimage1.jpg";
ContentValues values = new ContentValues();
values.put(Thumbnails.KIND, Thumbnails.FULL_SCREEN_KIND);
values.put(Thumbnails.IMAGE_ID, ContentUris.parseId(mRed));
values.put(Thumbnails.HEIGHT, 480);
values.put(Thumbnails.WIDTH, 320);
values.put(Thumbnails.DATA, externalImgPath);
// insert
Uri uri = mContentResolver.insert(Thumbnails.EXTERNAL_CONTENT_URI, values);
assertNotNull(uri);
// query
Cursor c = mContentResolver.query(uri, null, null, null, null);
assertEquals(1, c.getCount());
c.moveToFirst();
long id = c.getLong(c.getColumnIndex(Thumbnails._ID));
assertTrue(id > 0);
assertEquals(Thumbnails.FULL_SCREEN_KIND, c.getInt(c.getColumnIndex(Thumbnails.KIND)));
assertEquals(ContentUris.parseId(mRed), c.getLong(c.getColumnIndex(Thumbnails.IMAGE_ID)));
assertEquals(480, c.getInt(c.getColumnIndex(Thumbnails.HEIGHT)));
assertEquals(320, c.getInt(c.getColumnIndex(Thumbnails.WIDTH)));
assertEquals(externalImgPath, c.getString(c.getColumnIndex(Thumbnails.DATA)));
c.close();
// update
values.clear();
values.put(Thumbnails.KIND, Thumbnails.MICRO_KIND);
values.put(Thumbnails.IMAGE_ID, ContentUris.parseId(mBlue));
values.put(Thumbnails.HEIGHT, 50);
values.put(Thumbnails.WIDTH, 50);
values.put(Thumbnails.DATA, externalImgPath2);
assertEquals(1, mContentResolver.update(uri, values, null, null));
// delete
assertEquals(1, mContentResolver.delete(uri, null, null));
}
@Test
public void testThumbnailGenerationAndCleanup() throws Exception {
if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return;
final ContentResolver resolver = mContentResolver;
// insert an image
Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery);
Uri uri = Uri.parse(Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(),
"test description"));
long imageId = ContentUris.parseId(uri);
SystemClock.sleep(1000); // TODO: Remove sleep once getThumbnail has been fixed
assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null));
assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null));
// delete the source image and check that the thumbnail is gone too
mContentResolver.delete(uri, null /* where clause */, null /* where args */);
SystemClock.sleep(1000); // TODO: Remove sleep once getThumbnail has been fixed
assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null));
assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null));
// insert again
uri = Uri.parse(Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(),
"test description"));
imageId = ContentUris.parseId(uri);
SystemClock.sleep(1000); // TODO: Remove sleep once getThumbnail has been fixed
// query its thumbnail again
assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null));
assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null));
// update the media type
ContentValues values = new ContentValues();
values.put("media_type", 0);
assertEquals("unexpected number of updated rows",
1, mContentResolver.update(uri, values, null /* where */, null /* where args */));
SystemClock.sleep(1000);
// image was marked as regular file in the database, which should have deleted its thumbnail
assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null));
assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null));
// check source no longer exists as image
Cursor c = mContentResolver.query(uri,
null /* projection */, null /* where */, null /* where args */, null /* sort */);
assertFalse("source entry should be gone", c.moveToNext());
c.close();
// check source still exists as file
Uri fileUri = ContentUris.withAppendedId(
MediaStore.Files.getContentUri("external"),
Long.valueOf(uri.getLastPathSegment()));
c = mContentResolver.query(fileUri,
null /* projection */, null /* where */, null /* where args */, null /* sort */);
assertTrue("source entry is gone", c.moveToNext());
String sourcePath = c.getString(c.getColumnIndex("_data"));
c.close();
// clean up
mContentResolver.delete(fileUri, null /* where */, null /* where args */);
new File(sourcePath).delete();
}
@Test
public void testThumbnailOrderedQuery() throws Exception {
if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return;
Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery);
Uri url[] = new Uri[3];
try{
for (int i = 0; i < url.length; i++) {
url[i] = Uri.parse(
Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), null));
mRowsAdded.add(url[i]);
long origId = Long.parseLong(url[i].getLastPathSegment());
SystemClock.sleep(1000); // TODO: Remove sleep once getThumbnail has been fixed
Bitmap foo = MediaStore.Images.Thumbnails.getThumbnail(mContentResolver,
origId, Thumbnails.MICRO_KIND, null);
assertNotNull(foo);
}
// Remove one of the images, which will also delete any thumbnails
// If the image was deleted, we don't want to delete it again
if (mContentResolver.delete(url[1], null, null) > 0) {
mRowsAdded.remove(url[1]);
}
long removedId = Long.parseLong(url[1].getLastPathSegment());
long remainingId1 = Long.parseLong(url[0].getLastPathSegment());
long remainingId2 = Long.parseLong(url[2].getLastPathSegment());
// check if a thumbnail is still being returned for the image that was removed
Bitmap foo = MediaStore.Images.Thumbnails.getThumbnail(mContentResolver,
removedId, Thumbnails.MICRO_KIND, null);
assertNull(foo);
for (String order: new String[] { " ASC", " DESC" }) {
Cursor c = mContentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null,
MediaColumns._ID + order);
while (c.moveToNext()) {
long id = c.getLong(c.getColumnIndex(MediaColumns._ID));
foo = MediaStore.Images.Thumbnails.getThumbnail(
mContentResolver, id,
MediaStore.Images.Thumbnails.MICRO_KIND, null);
if (id == removedId) {
assertNull("unexpected bitmap with" + order + " ordering", foo);
} else if (id == remainingId1 || id == remainingId2) {
assertNotNull("missing bitmap with" + order + " ordering", foo);
}
}
c.close();
}
} catch (UnsupportedOperationException e) {
// the tests will be aborted because the image will be put in sdcard
fail("There is no sdcard attached! " + e.getMessage());
}
}
@Test
public void testInsertUpdateDelete() throws Exception {
final String displayName = "cts" + System.nanoTime();
final PendingParams params = new PendingParams(
mExternalImages, displayName, "image/png");
final Uri pendingUri = MediaStoreUtils.createPending(mContext, params);
final Uri finalUri;
try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) {
try (OutputStream out = session.openOutputStream()) {
writeImage(mLargestDimension, mLargestDimension, Color.RED, out);
}
finalUri = session.publish();
}
// Directly reading should be larger
final Bitmap full = ImageDecoder
.decodeBitmap(ImageDecoder.createSource(mContentResolver, finalUri));
assertEquals(mLargestDimension, full.getWidth());
assertEquals(mLargestDimension, full.getHeight());
{
// Thumbnail should be smaller
final Bitmap thumb = mContentResolver.loadThumbnail(finalUri, new Size(32, 32), null);
assertTrue(thumb.getWidth() < full.getWidth());
assertTrue(thumb.getHeight() < full.getHeight());
// Thumbnail should match contents
assertColorMostlyEquals(Color.RED, thumb.getPixel(16, 16));
}
// Verify legacy APIs still work
if (MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) {
for (int kind : new int[] {
MediaStore.Images.Thumbnails.MINI_KIND,
MediaStore.Images.Thumbnails.FULL_SCREEN_KIND,
MediaStore.Images.Thumbnails.MICRO_KIND
}) {
// Thumbnail should be smaller
final Bitmap thumb = MediaStore.Images.Thumbnails.getThumbnail(mContentResolver,
ContentUris.parseId(finalUri), kind, null);
assertTrue(thumb.getWidth() < full.getWidth());
assertTrue(thumb.getHeight() < full.getHeight());
// Thumbnail should match contents
assertColorMostlyEquals(Color.RED, thumb.getPixel(16, 16));
}
}
// Edit image contents
try (OutputStream out = mContentResolver.openOutputStream(finalUri)) {
writeImage(mLargestDimension, mLargestDimension, Color.BLUE, out);
}
// Wait a few moments for events to settle
SystemClock.sleep(1000);
{
// Thumbnail should match updated contents
final Bitmap thumb = mContentResolver.loadThumbnail(finalUri, new Size(32, 32), null);
assertColorMostlyEquals(Color.BLUE, thumb.getPixel(16, 16));
}
// Delete image contents
mContentResolver.delete(finalUri, null, null);
// Thumbnail should no longer exist
try {
mContentResolver.loadThumbnail(finalUri, new Size(32, 32), null);
fail("Funky; we somehow made a thumbnail out of nothing?");
} catch (FileNotFoundException expected) {
}
}
private static void writeImage(int width, int height, int color, OutputStream out) {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
canvas.drawColor(color);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
}
/**
* Since thumbnails might be bounced through a compression pass, we're okay
* if they're mostly equal.
*/
private static void assertColorMostlyEquals(int expected, int actual) {
assertEquals(Integer.toHexString(expected & 0xF0F0F0F0),
Integer.toHexString(actual & 0xF0F0F0F0));
}
}