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