1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.provider.cts; 18 19 import static android.provider.cts.MediaStoreTest.TAG; 20 import static android.provider.cts.ProviderTestUtils.assertExists; 21 import static android.provider.cts.ProviderTestUtils.assertNotExists; 22 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertFalse; 25 import static org.junit.Assert.assertNotNull; 26 import static org.junit.Assert.assertNull; 27 import static org.junit.Assert.assertTrue; 28 import static org.junit.Assert.fail; 29 30 import android.content.ContentResolver; 31 import android.content.ContentUris; 32 import android.content.ContentValues; 33 import android.content.Context; 34 import android.content.res.Configuration; 35 import android.content.res.Resources; 36 import android.database.Cursor; 37 import android.graphics.Bitmap; 38 import android.graphics.BitmapFactory; 39 import android.graphics.Canvas; 40 import android.graphics.Color; 41 import android.graphics.ImageDecoder; 42 import android.net.Uri; 43 import android.os.Environment; 44 import android.platform.test.annotations.Presubmit; 45 import android.provider.MediaStore; 46 import android.provider.MediaStore.Images.Media; 47 import android.provider.MediaStore.Images.Thumbnails; 48 import android.provider.MediaStore.MediaColumns; 49 import android.provider.cts.MediaStoreUtils.PendingParams; 50 import android.provider.cts.MediaStoreUtils.PendingSession; 51 import android.util.DisplayMetrics; 52 import android.util.Log; 53 import android.util.Size; 54 55 import androidx.test.InstrumentationRegistry; 56 57 import junit.framework.AssertionFailedError; 58 59 import org.junit.After; 60 import org.junit.Before; 61 import org.junit.Test; 62 import org.junit.runner.RunWith; 63 import org.junit.runners.Parameterized; 64 import org.junit.runners.Parameterized.Parameter; 65 import org.junit.runners.Parameterized.Parameters; 66 67 import java.io.File; 68 import java.io.FileNotFoundException; 69 import java.io.OutputStream; 70 import java.util.ArrayList; 71 72 @Presubmit 73 @RunWith(Parameterized.class) 74 public class MediaStore_Images_ThumbnailsTest { 75 private ArrayList<Uri> mRowsAdded; 76 77 private Context mContext; 78 private ContentResolver mContentResolver; 79 80 private Uri mExternalImages; 81 82 @Parameter(0) 83 public String mVolumeName; 84 85 private int mLargestDimension; 86 87 @Parameters data()88 public static Iterable<? extends Object> data() { 89 return ProviderTestUtils.getSharedVolumeNames(); 90 } 91 92 private Uri mRed; 93 private Uri mBlue; 94 95 @After tearDown()96 public void tearDown() throws Exception { 97 for (Uri row : mRowsAdded) { 98 try { 99 mContentResolver.delete(row, null, null); 100 } catch (UnsupportedOperationException e) { 101 // There is no way to delete rows from table "thumbnails" of internals database. 102 // ignores the exception and make the loop goes on 103 } 104 } 105 } 106 107 @Before setUp()108 public void setUp() throws Exception { 109 mContext = InstrumentationRegistry.getTargetContext(); 110 mContentResolver = mContext.getContentResolver(); 111 112 mRowsAdded = new ArrayList<Uri>(); 113 114 Log.d(TAG, "Using volume " + mVolumeName); 115 mExternalImages = MediaStore.Images.Media.getContentUri(mVolumeName); 116 117 final Resources res = mContext.getResources(); 118 final Configuration config = res.getConfiguration(); 119 mLargestDimension = (int) (Math.max(config.screenWidthDp, config.screenHeightDp) 120 * res.getDisplayMetrics().density); 121 } 122 prepareImages()123 private void prepareImages() throws Exception { 124 mRed = ProviderTestUtils.stageMedia(R.raw.scenery, mExternalImages); 125 mBlue = ProviderTestUtils.stageMedia(R.raw.scenery, mExternalImages); 126 mRowsAdded.add(mRed); 127 mRowsAdded.add(mBlue); 128 } 129 assertMostlyEquals(long expected, long actual, long delta)130 public static void assertMostlyEquals(long expected, long actual, long delta) { 131 if (Math.abs(expected - actual) > delta) { 132 throw new AssertionFailedError("Expected roughly " + expected + " but was " + actual); 133 } 134 } 135 136 @Test testQueryExternalThumbnails()137 public void testQueryExternalThumbnails() throws Exception { 138 if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return; 139 prepareImages(); 140 141 Cursor c = Thumbnails.queryMiniThumbnails(mContentResolver, 142 Thumbnails.EXTERNAL_CONTENT_URI, Thumbnails.MICRO_KIND, null); 143 int previousMicroKindCount = c.getCount(); 144 c.close(); 145 146 // add a thumbnail 147 final File file = new File(ProviderTestUtils.stageDir(MediaStore.VOLUME_EXTERNAL), 148 "testThumbnails.jpg"); 149 final String path = file.getAbsolutePath(); 150 ProviderTestUtils.stageFile(R.raw.scenery, file); 151 ContentValues values = new ContentValues(); 152 values.put(Thumbnails.KIND, Thumbnails.MINI_KIND); 153 values.put(Thumbnails.DATA, path); 154 values.put(Thumbnails.IMAGE_ID, ContentUris.parseId(mRed)); 155 Uri uri = mContentResolver.insert(Thumbnails.EXTERNAL_CONTENT_URI, values); 156 if (uri != null) { 157 mRowsAdded.add(uri); 158 } 159 160 // query with the uri of the thumbnail and the kind 161 c = Thumbnails.queryMiniThumbnails(mContentResolver, uri, Thumbnails.MINI_KIND, null); 162 c.moveToFirst(); 163 assertEquals(1, c.getCount()); 164 assertEquals(Thumbnails.MINI_KIND, c.getInt(c.getColumnIndex(Thumbnails.KIND))); 165 assertEquals(path, c.getString(c.getColumnIndex(Thumbnails.DATA))); 166 167 // query all thumbnails with other kind 168 c = Thumbnails.queryMiniThumbnails(mContentResolver, Thumbnails.EXTERNAL_CONTENT_URI, 169 Thumbnails.MICRO_KIND, null); 170 assertEquals(previousMicroKindCount, c.getCount()); 171 c.close(); 172 173 // query without kind 174 c = Thumbnails.query(mContentResolver, uri, null); 175 assertEquals(1, c.getCount()); 176 c.moveToFirst(); 177 assertEquals(Thumbnails.MINI_KIND, c.getInt(c.getColumnIndex(Thumbnails.KIND))); 178 assertEquals(path, c.getString(c.getColumnIndex(Thumbnails.DATA))); 179 c.close(); 180 } 181 182 @Test testQueryExternalMiniThumbnails()183 public void testQueryExternalMiniThumbnails() throws Exception { 184 if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return; 185 final ContentResolver resolver = mContentResolver; 186 187 // insert the image by bitmap 188 BitmapFactory.Options opts = new BitmapFactory.Options(); 189 opts.inTargetDensity = DisplayMetrics.DENSITY_XHIGH; 190 Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery,opts); 191 String stringUrl = null; 192 try{ 193 stringUrl = Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), null); 194 } catch (UnsupportedOperationException e) { 195 // the tests will be aborted because the image will be put in sdcard 196 fail("There is no sdcard attached! " + e.getMessage()); 197 } 198 assertNotNull(stringUrl); 199 Uri stringUri = Uri.parse(stringUrl); 200 mRowsAdded.add(stringUri); 201 202 // get the original image id and path 203 Cursor c = mContentResolver.query(stringUri, 204 new String[]{ Media._ID, Media.DATA }, null, null, null); 205 c.moveToFirst(); 206 long imageId = c.getLong(c.getColumnIndex(Media._ID)); 207 String imagePath = c.getString(c.getColumnIndex(Media.DATA)); 208 c.close(); 209 210 MediaStore.waitForIdle(mContext); 211 assertExists("image file does not exist", imagePath); 212 assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null)); 213 assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null)); 214 215 // deleting the image from the database also deletes the image file, and the 216 // corresponding entry in the thumbnail table, which in turn triggers deletion 217 // of the thumbnail file on disk 218 mContentResolver.delete(stringUri, null, null); 219 mRowsAdded.remove(stringUri); 220 221 MediaStore.waitForIdle(mContext); 222 assertNotExists("image file should no longer exist", imagePath); 223 assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null)); 224 assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null)); 225 226 // insert image, then delete it via the files table 227 stringUrl = Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), null); 228 c = mContentResolver.query(Uri.parse(stringUrl), 229 new String[]{ Media._ID, Media.DATA}, null, null, null); 230 c.moveToFirst(); 231 imageId = c.getLong(c.getColumnIndex(Media._ID)); 232 imagePath = c.getString(c.getColumnIndex(Media.DATA)); 233 c.close(); 234 assertExists("image file does not exist", imagePath); 235 Uri fileuri = MediaStore.Files.getContentUri("external", imageId); 236 mContentResolver.delete(fileuri, null, null); 237 assertNotExists("image file should no longer exist", imagePath); 238 } 239 240 @Test testGetContentUri()241 public void testGetContentUri() { 242 Cursor c = null; 243 assertNotNull(c = mContentResolver.query(Thumbnails.getContentUri("internal"), null, null, 244 null, null)); 245 c.close(); 246 assertNotNull(c = mContentResolver.query(Thumbnails.getContentUri(mVolumeName), null, null, 247 null, null)); 248 c.close(); 249 } 250 251 @Test testStoreImagesMediaExternal()252 public void testStoreImagesMediaExternal() throws Exception { 253 if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return; 254 prepareImages(); 255 256 final String externalImgPath = Environment.getExternalStorageDirectory() + 257 "/testimage.jpg"; 258 final String externalImgPath2 = Environment.getExternalStorageDirectory() + 259 "/testimage1.jpg"; 260 ContentValues values = new ContentValues(); 261 values.put(Thumbnails.KIND, Thumbnails.FULL_SCREEN_KIND); 262 values.put(Thumbnails.IMAGE_ID, ContentUris.parseId(mRed)); 263 values.put(Thumbnails.HEIGHT, 480); 264 values.put(Thumbnails.WIDTH, 320); 265 values.put(Thumbnails.DATA, externalImgPath); 266 267 // insert 268 Uri uri = mContentResolver.insert(Thumbnails.EXTERNAL_CONTENT_URI, values); 269 assertNotNull(uri); 270 271 // query 272 Cursor c = mContentResolver.query(uri, null, null, null, null); 273 assertEquals(1, c.getCount()); 274 c.moveToFirst(); 275 long id = c.getLong(c.getColumnIndex(Thumbnails._ID)); 276 assertTrue(id > 0); 277 assertEquals(Thumbnails.FULL_SCREEN_KIND, c.getInt(c.getColumnIndex(Thumbnails.KIND))); 278 assertEquals(ContentUris.parseId(mRed), c.getLong(c.getColumnIndex(Thumbnails.IMAGE_ID))); 279 assertEquals(480, c.getInt(c.getColumnIndex(Thumbnails.HEIGHT))); 280 assertEquals(320, c.getInt(c.getColumnIndex(Thumbnails.WIDTH))); 281 assertEquals(externalImgPath, c.getString(c.getColumnIndex(Thumbnails.DATA))); 282 c.close(); 283 284 // update 285 values.clear(); 286 values.put(Thumbnails.KIND, Thumbnails.MICRO_KIND); 287 values.put(Thumbnails.IMAGE_ID, ContentUris.parseId(mBlue)); 288 values.put(Thumbnails.HEIGHT, 50); 289 values.put(Thumbnails.WIDTH, 50); 290 values.put(Thumbnails.DATA, externalImgPath2); 291 assertEquals(1, mContentResolver.update(uri, values, null, null)); 292 293 // delete 294 assertEquals(1, mContentResolver.delete(uri, null, null)); 295 } 296 297 @Test testThumbnailGenerationAndCleanup()298 public void testThumbnailGenerationAndCleanup() throws Exception { 299 if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return; 300 final ContentResolver resolver = mContentResolver; 301 302 // insert an image 303 Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery); 304 Uri uri = Uri.parse(Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), 305 "test description")); 306 long imageId = ContentUris.parseId(uri); 307 308 MediaStore.waitForIdle(mContext); 309 assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null)); 310 assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null)); 311 312 // delete the source image and check that the thumbnail is gone too 313 mContentResolver.delete(uri, null /* where clause */, null /* where args */); 314 315 MediaStore.waitForIdle(mContext); 316 assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null)); 317 assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null)); 318 319 // insert again 320 uri = Uri.parse(Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), 321 "test description")); 322 imageId = ContentUris.parseId(uri); 323 324 // query its thumbnail again 325 MediaStore.waitForIdle(mContext); 326 assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null)); 327 assertNotNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null)); 328 329 // update the media type 330 ContentValues values = new ContentValues(); 331 values.put("media_type", 0); 332 assertEquals("unexpected number of updated rows", 333 1, mContentResolver.update(uri, values, null /* where */, null /* where args */)); 334 335 // image was marked as regular file in the database, which should have deleted its thumbnail 336 MediaStore.waitForIdle(mContext); 337 assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MINI_KIND, null)); 338 assertNull(Thumbnails.getThumbnail(resolver, imageId, Thumbnails.MICRO_KIND, null)); 339 340 // check source no longer exists as image 341 Cursor c = mContentResolver.query(uri, 342 null /* projection */, null /* where */, null /* where args */, null /* sort */); 343 assertFalse("source entry should be gone", c.moveToNext()); 344 c.close(); 345 346 // check source still exists as file 347 Uri fileUri = ContentUris.withAppendedId( 348 MediaStore.Files.getContentUri("external"), 349 Long.valueOf(uri.getLastPathSegment())); 350 c = mContentResolver.query(fileUri, 351 null /* projection */, null /* where */, null /* where args */, null /* sort */); 352 assertTrue("source entry is gone", c.moveToNext()); 353 String sourcePath = c.getString(c.getColumnIndex("_data")); 354 c.close(); 355 356 // clean up 357 mContentResolver.delete(fileUri, null /* where */, null /* where args */); 358 new File(sourcePath).delete(); 359 } 360 361 @Test testThumbnailOrderedQuery()362 public void testThumbnailOrderedQuery() throws Exception { 363 if (!MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) return; 364 365 Bitmap src = BitmapFactory.decodeResource(mContext.getResources(), R.raw.scenery); 366 Uri url[] = new Uri[3]; 367 try{ 368 for (int i = 0; i < url.length; i++) { 369 url[i] = Uri.parse( 370 Media.insertImage(mContentResolver, src, "cts" + System.nanoTime(), null)); 371 mRowsAdded.add(url[i]); 372 long origId = Long.parseLong(url[i].getLastPathSegment()); 373 MediaStore.waitForIdle(mContext); 374 Bitmap foo = MediaStore.Images.Thumbnails.getThumbnail(mContentResolver, 375 origId, Thumbnails.MICRO_KIND, null); 376 assertNotNull(foo); 377 } 378 379 // Remove one of the images, which will also delete any thumbnails 380 // If the image was deleted, we don't want to delete it again 381 if (mContentResolver.delete(url[1], null, null) > 0) { 382 mRowsAdded.remove(url[1]); 383 } 384 385 long removedId = Long.parseLong(url[1].getLastPathSegment()); 386 long remainingId1 = Long.parseLong(url[0].getLastPathSegment()); 387 long remainingId2 = Long.parseLong(url[2].getLastPathSegment()); 388 389 // check if a thumbnail is still being returned for the image that was removed 390 MediaStore.waitForIdle(mContext); 391 Bitmap foo = MediaStore.Images.Thumbnails.getThumbnail(mContentResolver, 392 removedId, Thumbnails.MICRO_KIND, null); 393 assertNull(foo); 394 395 for (String order: new String[] { " ASC", " DESC" }) { 396 Cursor c = mContentResolver.query( 397 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, 398 MediaColumns._ID + order); 399 while (c.moveToNext()) { 400 long id = c.getLong(c.getColumnIndex(MediaColumns._ID)); 401 MediaStore.waitForIdle(mContext); 402 foo = MediaStore.Images.Thumbnails.getThumbnail( 403 mContentResolver, id, 404 MediaStore.Images.Thumbnails.MICRO_KIND, null); 405 if (id == removedId) { 406 assertNull("unexpected bitmap with" + order + " ordering", foo); 407 } else if (id == remainingId1 || id == remainingId2) { 408 assertNotNull("missing bitmap with" + order + " ordering", foo); 409 } 410 } 411 c.close(); 412 } 413 } catch (UnsupportedOperationException e) { 414 // the tests will be aborted because the image will be put in sdcard 415 fail("There is no sdcard attached! " + e.getMessage()); 416 } 417 } 418 419 @Test testInsertUpdateDelete()420 public void testInsertUpdateDelete() throws Exception { 421 final String displayName = "cts" + System.nanoTime(); 422 final PendingParams params = new PendingParams( 423 mExternalImages, displayName, "image/png"); 424 final Uri pendingUri = MediaStoreUtils.createPending(mContext, params); 425 final Uri finalUri; 426 try (PendingSession session = MediaStoreUtils.openPending(mContext, pendingUri)) { 427 try (OutputStream out = session.openOutputStream()) { 428 writeImage(mLargestDimension, mLargestDimension, Color.RED, out); 429 } 430 finalUri = session.publish(); 431 } 432 433 // Directly reading should be larger 434 final Bitmap full = ImageDecoder 435 .decodeBitmap(ImageDecoder.createSource(mContentResolver, finalUri)); 436 assertEquals(mLargestDimension, full.getWidth()); 437 assertEquals(mLargestDimension, full.getHeight()); 438 439 { 440 // Thumbnail should be smaller 441 MediaStore.waitForIdle(mContext); 442 final Bitmap thumb = mContentResolver.loadThumbnail(finalUri, new Size(32, 32), null); 443 assertTrue(thumb.getWidth() < full.getWidth()); 444 assertTrue(thumb.getHeight() < full.getHeight()); 445 446 // Thumbnail should match contents 447 assertColorMostlyEquals(Color.RED, thumb.getPixel(16, 16)); 448 } 449 450 // Verify legacy APIs still work 451 if (MediaStore.VOLUME_EXTERNAL.equals(mVolumeName)) { 452 for (int kind : new int[] { 453 MediaStore.Images.Thumbnails.MINI_KIND, 454 MediaStore.Images.Thumbnails.FULL_SCREEN_KIND, 455 MediaStore.Images.Thumbnails.MICRO_KIND 456 }) { 457 // Thumbnail should be smaller 458 MediaStore.waitForIdle(mContext); 459 final Bitmap thumb = MediaStore.Images.Thumbnails.getThumbnail(mContentResolver, 460 ContentUris.parseId(finalUri), kind, null); 461 assertTrue(thumb.getWidth() < full.getWidth()); 462 assertTrue(thumb.getHeight() < full.getHeight()); 463 464 // Thumbnail should match contents 465 assertColorMostlyEquals(Color.RED, thumb.getPixel(16, 16)); 466 } 467 } 468 469 // Edit image contents 470 try (OutputStream out = mContentResolver.openOutputStream(finalUri)) { 471 writeImage(mLargestDimension, mLargestDimension, Color.BLUE, out); 472 } 473 474 // Wait a few moments for events to settle 475 MediaStore.waitForIdle(mContext); 476 477 { 478 // Thumbnail should match updated contents 479 MediaStore.waitForIdle(mContext); 480 final Bitmap thumb = mContentResolver.loadThumbnail(finalUri, new Size(32, 32), null); 481 assertColorMostlyEquals(Color.BLUE, thumb.getPixel(16, 16)); 482 } 483 484 // Delete image contents 485 mContentResolver.delete(finalUri, null, null); 486 487 // Thumbnail should no longer exist 488 try { 489 MediaStore.waitForIdle(mContext); 490 mContentResolver.loadThumbnail(finalUri, new Size(32, 32), null); 491 fail("Funky; we somehow made a thumbnail out of nothing?"); 492 } catch (FileNotFoundException expected) { 493 } 494 } 495 496 private static void writeImage(int width, int height, int color, OutputStream out) { 497 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 498 final Canvas canvas = new Canvas(bitmap); 499 canvas.drawColor(color); 500 bitmap.compress(Bitmap.CompressFormat.PNG, 90, out); 501 } 502 503 /** 504 * Since thumbnails might be bounced through a compression pass, we're okay 505 * if they're mostly equal. 506 */ 507 private static void assertColorMostlyEquals(int expected, int actual) { 508 assertEquals(Integer.toHexString(expected & 0xF0F0F0F0), 509 Integer.toHexString(actual & 0xF0F0F0F0)); 510 } 511 } 512