• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.graphics.cts;
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertNotEquals;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertSame;
24 import static org.junit.Assert.assertTrue;
25 import static org.junit.Assert.fail;
26 import static org.junit.Assume.assumeTrue;
27 
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.res.AssetFileDescriptor;
31 import android.content.res.AssetManager;
32 import android.content.res.Resources;
33 import android.graphics.Bitmap;
34 import android.graphics.BitmapFactory;
35 import android.graphics.Canvas;
36 import android.graphics.Color;
37 import android.graphics.ColorSpace;
38 import android.graphics.ImageDecoder;
39 import android.graphics.ImageDecoder.DecodeException;
40 import android.graphics.ImageDecoder.OnPartialImageListener;
41 import android.graphics.PixelFormat;
42 import android.graphics.PostProcessor;
43 import android.graphics.Rect;
44 import android.graphics.drawable.BitmapDrawable;
45 import android.graphics.drawable.Drawable;
46 import android.graphics.drawable.NinePatchDrawable;
47 import android.media.MediaCodecInfo;
48 import android.media.MediaCodecList;
49 import android.media.MediaFormat;
50 import android.net.Uri;
51 import android.os.Build;
52 import android.os.SystemProperties;
53 import android.util.DisplayMetrics;
54 import android.util.Size;
55 import android.util.TypedValue;
56 
57 import androidx.core.content.FileProvider;
58 import androidx.test.InstrumentationRegistry;
59 import androidx.test.filters.LargeTest;
60 import androidx.test.filters.RequiresDevice;
61 
62 import com.android.compatibility.common.util.ApiLevelUtil;
63 import com.android.compatibility.common.util.BitmapUtils;
64 import com.android.compatibility.common.util.CddTest;
65 import com.android.compatibility.common.util.MediaUtils;
66 
67 import org.junit.Test;
68 import org.junit.runner.RunWith;
69 
70 import java.io.ByteArrayOutputStream;
71 import java.io.File;
72 import java.io.FileNotFoundException;
73 import java.io.FileOutputStream;
74 import java.io.IOException;
75 import java.io.InputStream;
76 import java.io.OutputStream;
77 import java.nio.ByteBuffer;
78 import java.util.ArrayList;
79 import java.util.Arrays;
80 import java.util.Collection;
81 import java.util.List;
82 import java.util.concurrent.Callable;
83 import java.util.function.IntFunction;
84 import java.util.function.Supplier;
85 import java.util.function.ToIntFunction;
86 
87 import junitparams.JUnitParamsRunner;
88 import junitparams.Parameters;
89 
90 @RunWith(JUnitParamsRunner.class)
91 public class ImageDecoderTest {
92     static final class Record {
93         public final int resId;
94         public final int width;
95         public final int height;
96         public final boolean isGray;
97         public final boolean hasAlpha;
98         public final String mimeType;
99         public final ColorSpace colorSpace;
100 
Record(int resId, int width, int height, String mimeType, boolean isGray, boolean hasAlpha, ColorSpace colorSpace)101         Record(int resId, int width, int height, String mimeType, boolean isGray,
102                 boolean hasAlpha, ColorSpace colorSpace) {
103             this.resId    = resId;
104             this.width    = width;
105             this.height   = height;
106             this.mimeType = mimeType;
107             this.isGray   = isGray;
108             this.hasAlpha = hasAlpha;
109             this.colorSpace = colorSpace;
110         }
111     }
112 
113     private static final ColorSpace sSRGB = ColorSpace.get(ColorSpace.Named.SRGB);
114 
getRecords()115     static Record[] getRecords() {
116         ArrayList<Record> records = new ArrayList<>(Arrays.asList(new Record[] {
117                 new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", false, false, sSRGB),
118                 new Record(R.drawable.grayscale_jpg, 128, 128, "image/jpeg", true, false, sSRGB),
119                 new Record(R.drawable.png_test, 640, 480, "image/png", false, false, sSRGB),
120                 new Record(R.drawable.gif_test, 320, 240, "image/gif", false, false, sSRGB),
121                 new Record(R.drawable.bmp_test, 320, 240, "image/bmp", false, false, sSRGB),
122                 new Record(R.drawable.webp_test, 640, 480, "image/webp", false, false, sSRGB),
123                 new Record(R.drawable.google_chrome, 256, 256, "image/x-ico", false, true, sSRGB),
124                 new Record(R.drawable.color_wheel, 128, 128, "image/x-ico", false, true, sSRGB),
125                 new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", false, false, sSRGB)
126         }));
127         if (ImageDecoder.isMimeTypeSupported("image/heif")) {
128             // HEIF support is optional when HEVC decoder is not supported.
129             records.add(new Record(R.raw.heifwriter_input, 1920, 1080, "image/heif", false, false,
130                                    sSRGB));
131         }
132         if (ImageDecoder.isMimeTypeSupported("image/avif")) {
133             records.add(new Record(R.raw.avif_yuv_420_8bit, 120, 160, "image/avif", false, false,
134                                    sSRGB));
135         }
136         return records.toArray(new Record[] {});
137     }
138 
139     // offset is how many bytes to offset the beginning of the image.
140     // extra is how many bytes to append at the end.
getAsByteArray(int resId, int offset, int extra)141     private static byte[] getAsByteArray(int resId, int offset, int extra) {
142         ByteArrayOutputStream output = new ByteArrayOutputStream();
143         writeToStream(output, resId, offset, extra);
144         return output.toByteArray();
145     }
146 
writeToStream(OutputStream output, int resId, int offset, int extra)147     static void writeToStream(OutputStream output, int resId, int offset, int extra) {
148         InputStream input = getResources().openRawResource(resId);
149         byte[] buffer = new byte[4096];
150         int bytesRead;
151         try {
152             for (int i = 0; i < offset; ++i) {
153                 output.write(0);
154             }
155 
156             while ((bytesRead = input.read(buffer)) != -1) {
157                 output.write(buffer, 0, bytesRead);
158             }
159 
160             for (int i = 0; i < extra; ++i) {
161                 output.write(0);
162             }
163 
164             input.close();
165         } catch (IOException e) {
166             fail();
167         }
168     }
169 
getAsByteArray(int resId)170     static byte[] getAsByteArray(int resId) {
171         return getAsByteArray(resId, 0, 0);
172     }
173 
getAsByteBufferWrap(int resId)174     private ByteBuffer getAsByteBufferWrap(int resId) {
175         byte[] buffer = getAsByteArray(resId);
176         return ByteBuffer.wrap(buffer);
177     }
178 
getAsDirectByteBuffer(int resId)179     private ByteBuffer getAsDirectByteBuffer(int resId) {
180         byte[] buffer = getAsByteArray(resId);
181         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(buffer.length);
182         byteBuffer.put(buffer);
183         byteBuffer.position(0);
184         return byteBuffer;
185     }
186 
getAsReadOnlyByteBuffer(int resId)187     private ByteBuffer getAsReadOnlyByteBuffer(int resId) {
188         return getAsByteBufferWrap(resId).asReadOnlyBuffer();
189     }
190 
getAsFile(int resId)191     private File getAsFile(int resId) {
192         File file = null;
193         try {
194             Context context = InstrumentationRegistry.getTargetContext();
195             File dir = new File(context.getFilesDir(), "images");
196             dir.mkdirs();
197             file = new File(dir, "test_file" + resId);
198             if (!file.createNewFile() && !file.exists()) {
199                 fail("Failed to create new File!");
200             }
201 
202             FileOutputStream output = new FileOutputStream(file);
203             writeToStream(output, resId, 0, 0);
204             output.close();
205 
206         } catch (IOException e) {
207             fail("Failed with exception " + e);
208             return null;
209         }
210         return file;
211     }
212 
getAsFileUri(int resId)213     private Uri getAsFileUri(int resId) {
214         return Uri.fromFile(getAsFile(resId));
215     }
216 
getAsContentUri(int resId)217     private Uri getAsContentUri(int resId) {
218         Context context = InstrumentationRegistry.getTargetContext();
219         return FileProvider.getUriForFile(context,
220                 "android.graphics.cts.fileprovider", getAsFile(resId));
221     }
222 
getAsCallable(int resId)223     private Callable<AssetFileDescriptor> getAsCallable(int resId) {
224         final Context context = InstrumentationRegistry.getTargetContext();
225         final Uri uri = getAsContentUri(resId);
226         return () -> {
227             return context.getContentResolver().openAssetFileDescriptor(uri, "r");
228         };
229     }
230 
231     private interface SourceCreator extends IntFunction<ImageDecoder.Source> {};
232 
233     private SourceCreator[] mCreators = new SourceCreator[] {
234             resId -> ImageDecoder.createSource(getAsByteArray(resId)),
235             resId -> ImageDecoder.createSource(getAsByteBufferWrap(resId)),
236             resId -> ImageDecoder.createSource(getAsDirectByteBuffer(resId)),
237             resId -> ImageDecoder.createSource(getAsReadOnlyByteBuffer(resId)),
238             resId -> ImageDecoder.createSource(getAsFile(resId)),
239             resId -> ImageDecoder.createSource(getAsCallable(resId)),
240     };
241 
242     private interface UriCreator extends IntFunction<Uri> {};
243 
244     private UriCreator[] mUriCreators = new UriCreator[] {
245             resId -> Utils.getAsResourceUri(resId),
246             resId -> getAsFileUri(resId),
247             resId -> getAsContentUri(resId),
248     };
249 
250     @Test
251     @RequiresDevice
testDecode10BitHeif()252     public void testDecode10BitHeif() {
253         assumeTrue(
254             "This test only applies to Android 13 (T) or newer. Skip the test.",
255             ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.TIRAMISU));
256         assumeTrue(
257             "Test only applies to VNDK version 33 (T) or newer. Skip the test.",
258             SystemProperties.getInt("ro.vndk.version", Build.VERSION_CODES.CUR_DEVELOPMENT)
259                 >= Build.VERSION_CODES.TIRAMISU);
260         assumeTrue("No 10-bit HEVC decoder, skip the test.", has10BitHEVCDecoder());
261 
262         Bitmap.Config expectedConfig = Bitmap.Config.RGBA_1010102;
263 
264         // For TVs, even if the device advertises that 10 bits profile is supported, the output
265         // format might not be CPU readable, but the video can still be displayed. When the TV's
266         // hevc decoder doesn't support YUVP010 format, then the color type of output falls back
267         // to RGBA_8888 automatically.
268         if (MediaUtils.isTv() && !hasHEVCDecoderSupportsYUVP010()) {
269             expectedConfig = Bitmap.Config.ARGB_8888;
270         }
271 
272         try {
273             ImageDecoder.Source src = ImageDecoder
274                 .createSource(getResources(), R.raw.heifimage_10bit);
275             assertNotNull(src);
276             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
277                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
278             });
279             assertNotNull(bm);
280             assertEquals(4096, bm.getWidth());
281             assertEquals(3072, bm.getHeight());
282             assertEquals(expectedConfig, bm.getConfig());
283         } catch (IOException e) {
284             fail("Failed with exception " + e);
285         }
286     }
287 
288     @Test
289     @CddTest(requirements = {"5.1.5/C-0-7"})
290     @RequiresDevice
testDecode10BitAvif()291     public void testDecode10BitAvif() {
292         assumeTrue("AVIF is not supported on this device, skip this test.",
293                 ImageDecoder.isMimeTypeSupported("image/avif"));
294 
295         try {
296             ImageDecoder.Source src = ImageDecoder
297                 .createSource(getResources(), R.raw.avif_yuv_420_10bit);
298             assertNotNull(src);
299             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
300                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
301             });
302             assertNotNull(bm);
303             assertEquals(120, bm.getWidth());
304             assertEquals(160, bm.getHeight());
305             assertEquals(Bitmap.Config.RGBA_1010102, bm.getConfig());
306         } catch (IOException e) {
307             fail("Failed with exception " + e);
308         }
309     }
310 
311     @Test
312     @RequiresDevice
testDecode10BitHeifWithLowRam()313     public void testDecode10BitHeifWithLowRam() {
314         assumeTrue(
315             "This test only applies to Android 13 (T) or newer. Skip the test.",
316             ApiLevelUtil.isFirstApiAtLeast(Build.VERSION_CODES.TIRAMISU));
317         assumeTrue(
318             "Test only applies to VNDK version 33 (T) or newer. Skip the test.",
319             SystemProperties.getInt("ro.vndk.version", Build.VERSION_CODES.CUR_DEVELOPMENT)
320                 >= Build.VERSION_CODES.TIRAMISU);
321         assumeTrue("No 10-bit HEVC decoder, skip the test.", has10BitHEVCDecoder());
322 
323         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), R.raw.heifimage_10bit);
324         assertNotNull(src);
325         try {
326             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
327                 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
328                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
329             });
330             assertNotNull(bm);
331             assertEquals(4096, bm.getWidth());
332             assertEquals(3072, bm.getHeight());
333             assertEquals(Bitmap.Config.RGB_565, bm.getConfig());
334         } catch (IOException e) {
335             fail("Failed with exception " + e);
336         }
337     }
338 
339     @Test
340     @CddTest(requirements = {"5.1.5/C-0-7"})
341     @RequiresDevice
testDecode10BitAvifWithLowRam()342     public void testDecode10BitAvifWithLowRam() {
343         assumeTrue("AVIF is not supported on this device, skip this test.",
344                 ImageDecoder.isMimeTypeSupported("image/avif"));
345 
346         ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
347                 R.raw.avif_yuv_420_10bit);
348         assertNotNull(src);
349         try {
350             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
351                 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
352                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
353             });
354             assertNotNull(bm);
355             assertEquals(120, bm.getWidth());
356             assertEquals(160, bm.getHeight());
357             assertEquals(Bitmap.Config.RGB_565, bm.getConfig());
358         } catch (IOException e) {
359             fail("Failed with exception " + e);
360         }
361     }
362 
363     @Test
364     @Parameters(method = "getRecords")
testUris(Record record)365     public void testUris(Record record) {
366         int resId = record.resId;
367         String name = getResources().getResourceEntryName(resId);
368         for (UriCreator f : mUriCreators) {
369             ImageDecoder.Source src = null;
370             Uri uri = f.apply(resId);
371             String fullName = name + ": " + uri.toString();
372             src = ImageDecoder.createSource(getContentResolver(), uri);
373 
374             assertNotNull("failed to create Source for " + fullName, src);
375             try {
376                 Drawable d = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
377                     decoder.setOnPartialImageListener((e) -> {
378                         fail("error for image " + fullName + ":\n" + e);
379                         return false;
380                     });
381                 });
382                 assertNotNull("failed to create drawable for " + fullName, d);
383             } catch (IOException e) {
384                 fail("exception for image " + fullName + ":\n" + e);
385             }
386         }
387     }
388 
getResources()389     private static Resources getResources() {
390         return InstrumentationRegistry.getTargetContext().getResources();
391     }
392 
getContentResolver()393     private static ContentResolver getContentResolver() {
394         return InstrumentationRegistry.getTargetContext().getContentResolver();
395     }
396 
397     @Test
398     @Parameters(method = "getRecords")
testInfo(Record record)399     public void testInfo(Record record) {
400         for (SourceCreator f : mCreators) {
401             ImageDecoder.Source src = f.apply(record.resId);
402             assertNotNull(src);
403             try {
404                 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
405                     assertEquals(record.width,  info.getSize().getWidth());
406                     assertEquals(record.height, info.getSize().getHeight());
407                     assertEquals(record.mimeType, info.getMimeType());
408                     assertSame(record.colorSpace, info.getColorSpace());
409                 });
410             } catch (IOException e) {
411                 fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
412             }
413         }
414     }
415 
416     @Test
417     @Parameters(method = "getRecords")
testDecodeDrawable(Record record)418     public void testDecodeDrawable(Record record) {
419         for (SourceCreator f : mCreators) {
420             ImageDecoder.Source src = f.apply(record.resId);
421             assertNotNull(src);
422 
423             try {
424                 Drawable drawable = ImageDecoder.decodeDrawable(src);
425                 assertNotNull(drawable);
426                 assertEquals(record.width,  drawable.getIntrinsicWidth());
427                 assertEquals(record.height, drawable.getIntrinsicHeight());
428             } catch (IOException e) {
429                 fail("Failed with exception " + e);
430             }
431         }
432     }
433 
434     @Test
435     @Parameters(method = "getRecords")
testDecodeBitmap(Record record)436     public void testDecodeBitmap(Record record) {
437         for (SourceCreator f : mCreators) {
438             ImageDecoder.Source src = f.apply(record.resId);
439             assertNotNull(src);
440 
441             try {
442                 Bitmap bm = ImageDecoder.decodeBitmap(src);
443                 assertNotNull(bm);
444                 assertEquals(record.width, bm.getWidth());
445                 assertEquals(record.height, bm.getHeight());
446                 assertFalse(bm.isMutable());
447                 // FIXME: This may change for small resources, etc.
448                 assertEquals(Bitmap.Config.HARDWARE, bm.getConfig());
449             } catch (IOException e) {
450                 fail("Failed with exception " + e);
451             }
452         }
453     }
454 
455     // Return a single Record for simple tests.
getRecord()456     private Record getRecord() {
457         return ((Record[]) getRecords())[0];
458     }
459 
460     @Test(expected = IllegalArgumentException.class)
testSetBogusAllocator()461     public void testSetBogusAllocator() {
462         ImageDecoder.Source src = mCreators[0].apply(getRecord().resId);
463         try {
464             ImageDecoder.decodeBitmap(src, (decoder, info, s) -> decoder.setAllocator(15));
465         } catch (IOException e) {
466             fail("Failed with exception " + e);
467         }
468     }
469 
470     private static final int[] ALLOCATORS = new int[] {
471         ImageDecoder.ALLOCATOR_SOFTWARE,
472         ImageDecoder.ALLOCATOR_SHARED_MEMORY,
473         ImageDecoder.ALLOCATOR_HARDWARE,
474         ImageDecoder.ALLOCATOR_DEFAULT,
475     };
476 
477     @Test
testGetAllocator()478     public void testGetAllocator() {
479         final int resId = getRecord().resId;
480         ImageDecoder.Source src = mCreators[0].apply(resId);
481         try {
482             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
483                 assertEquals(ImageDecoder.ALLOCATOR_DEFAULT, decoder.getAllocator());
484                 for (int allocator : ALLOCATORS) {
485                     decoder.setAllocator(allocator);
486                     assertEquals(allocator, decoder.getAllocator());
487                 }
488             });
489         } catch (IOException e) {
490             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
491         }
492     }
493 
paramsForTestSetAllocatorDecodeBitmap()494     private Collection<Object[]> paramsForTestSetAllocatorDecodeBitmap() {
495         boolean[] trueFalse = new boolean[] { true, false };
496         List<Object[]> temp = new ArrayList<>();
497         for (Object record : getRecords()) {
498             for (int allocator : ALLOCATORS) {
499                 for (boolean doCrop : trueFalse) {
500                     for (boolean doScale : trueFalse) {
501                         temp.add(new Object[]{record, allocator, doCrop, doScale});
502                     }
503                 }
504             }
505         }
506         return temp;
507     }
508 
509     @Test
510     @Parameters(method = "paramsForTestSetAllocatorDecodeBitmap")
testSetAllocatorDecodeBitmap(Record record, int allocator, boolean doCrop, boolean doScale)511     public void testSetAllocatorDecodeBitmap(Record record, int allocator, boolean doCrop,
512                                              boolean doScale) {
513         class Listener implements ImageDecoder.OnHeaderDecodedListener {
514             public int allocator;
515             public boolean doCrop;
516             public boolean doScale;
517             @Override
518             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
519                                         ImageDecoder.Source src) {
520                 decoder.setAllocator(allocator);
521                 if (doScale) {
522                     decoder.setTargetSampleSize(2);
523                 }
524                 if (doCrop) {
525                     decoder.setCrop(new Rect(1, 1, info.getSize().getWidth()  / 2 - 1,
526                                                    info.getSize().getHeight() / 2 - 1));
527                 }
528             }
529         };
530         Listener l = new Listener();
531 
532         // This test relies on ImageDecoder *not* scaling to account for density.
533         // Temporarily change the DisplayMetrics to prevent that scaling.
534         Resources res = getResources();
535         final int originalDensity = res.getDisplayMetrics().densityDpi;
536         res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_DEFAULT;
537         ImageDecoder.Source src = ImageDecoder.createSource(res, record.resId);
538         assertNotNull(src);
539         l.doCrop = doCrop;
540         l.doScale = doScale;
541         l.allocator = allocator;
542 
543         Bitmap bm = null;
544         try {
545             bm = ImageDecoder.decodeBitmap(src, l);
546         } catch (IOException e) {
547             fail("Failed " + Utils.getAsResourceUri(record.resId)
548                     + " with exception " + e);
549         } finally {
550             res.getDisplayMetrics().densityDpi = originalDensity;
551         }
552         assertNotNull(bm);
553 
554         switch (allocator) {
555             case ImageDecoder.ALLOCATOR_SHARED_MEMORY:
556                 // For a Bitmap backed by shared memory, asShared will return
557                 // the same Bitmap.
558                 assertSame(bm, bm.asShared());
559 
560                 // fallthrough
561             case ImageDecoder.ALLOCATOR_SOFTWARE:
562                 assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
563 
564                 if (!doScale && !doCrop) {
565                     BitmapFactory.Options options = new BitmapFactory.Options();
566                     options.inScaled = false;
567                     Bitmap reference = BitmapFactory.decodeResource(res,
568                             record.resId, options);
569                     assertNotNull(reference);
570                     assertTrue(BitmapUtils.compareBitmaps(bm, reference));
571                 }
572                 break;
573             default:
574                 String name = Utils.getAsResourceUri(record.resId).toString();
575                 assertEquals("image " + name + "; allocator: " + allocator,
576                              Bitmap.Config.HARDWARE, bm.getConfig());
577                 break;
578         }
579     }
580 
581     @Test
testGetUnpremul()582     public void testGetUnpremul() {
583         final int resId = getRecord().resId;
584         ImageDecoder.Source src = mCreators[0].apply(resId);
585         try {
586             ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
587                 assertFalse(decoder.isUnpremultipliedRequired());
588 
589                 decoder.setUnpremultipliedRequired(true);
590                 assertTrue(decoder.isUnpremultipliedRequired());
591 
592                 decoder.setUnpremultipliedRequired(false);
593                 assertFalse(decoder.isUnpremultipliedRequired());
594             });
595         } catch (IOException e) {
596             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
597         }
598     }
599 
600     @Test
testUnpremul()601     public void testUnpremul() {
602         int[] resIds = new int[] { R.drawable.png_test, R.drawable.alpha };
603         boolean[] hasAlpha = new boolean[] { false,     true };
604         for (int i = 0; i < resIds.length; ++i) {
605             for (SourceCreator f : mCreators) {
606                 // Normal decode
607                 ImageDecoder.Source src = f.apply(resIds[i]);
608                 assertNotNull(src);
609 
610                 try {
611                     Bitmap normal = ImageDecoder.decodeBitmap(src);
612                     assertNotNull(normal);
613                     assertEquals(normal.hasAlpha(), hasAlpha[i]);
614                     assertEquals(normal.isPremultiplied(), hasAlpha[i]);
615 
616                     // Require unpremul
617                     src = f.apply(resIds[i]);
618                     assertNotNull(src);
619 
620                     Bitmap unpremul = ImageDecoder.decodeBitmap(src,
621                             (decoder, info, s) -> decoder.setUnpremultipliedRequired(true));
622                     assertNotNull(unpremul);
623                     assertEquals(unpremul.hasAlpha(), hasAlpha[i]);
624                     assertFalse(unpremul.isPremultiplied());
625                 } catch (IOException e) {
626                     fail("Failed with exception " + e);
627                 }
628             }
629         }
630     }
631 
632     @Test
testGetPostProcessor()633     public void testGetPostProcessor() {
634         PostProcessor[] processors = new PostProcessor[] {
635                 (canvas) -> PixelFormat.UNKNOWN,
636                 (canvas) -> PixelFormat.UNKNOWN,
637                 null,
638         };
639         final int resId = getRecord().resId;
640         ImageDecoder.Source src = mCreators[0].apply(resId);
641         try {
642             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
643                 assertNull(decoder.getPostProcessor());
644 
645                 for (PostProcessor pp : processors) {
646                     decoder.setPostProcessor(pp);
647                     assertSame(pp, decoder.getPostProcessor());
648                 }
649             });
650         } catch (IOException e) {
651             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
652         }
653     }
654 
655     @Test
656     @Parameters(method = "getRecords")
testPostProcessor(Record record)657     public void testPostProcessor(Record record) {
658         class Listener implements ImageDecoder.OnHeaderDecodedListener {
659             public boolean requireSoftware;
660             @Override
661             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
662                                         ImageDecoder.Source src) {
663                 if (requireSoftware) {
664                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
665                 }
666                 decoder.setPostProcessor((canvas) -> {
667                     canvas.drawColor(Color.BLACK);
668                     return PixelFormat.OPAQUE;
669                 });
670             }
671         };
672         Listener l = new Listener();
673         boolean trueFalse[] = new boolean[] { true, false };
674         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), record.resId);
675         assertNotNull(src);
676         for (boolean requireSoftware : trueFalse) {
677             l.requireSoftware = requireSoftware;
678 
679             Bitmap bitmap = null;
680             try {
681                 bitmap = ImageDecoder.decodeBitmap(src, l);
682             } catch (IOException e) {
683                 fail("Failed with exception " + e);
684             }
685             assertNotNull(bitmap);
686             assertFalse(bitmap.isMutable());
687             if (requireSoftware) {
688                 assertNotEquals(Bitmap.Config.HARDWARE, bitmap.getConfig());
689                 for (int x = 0; x < bitmap.getWidth(); ++x) {
690                     for (int y = 0; y < bitmap.getHeight(); ++y) {
691                         int color = bitmap.getPixel(x, y);
692                         assertEquals("pixel at (" + x + ", " + y + ") does not match!",
693                                 color, Color.BLACK);
694                     }
695                 }
696             } else {
697                 assertEquals(bitmap.getConfig(), Bitmap.Config.HARDWARE);
698             }
699         }
700     }
701 
702     @Test
testNinepatchWithDensityNone()703     public void testNinepatchWithDensityNone() {
704         Resources res = getResources();
705         TypedValue value = new TypedValue();
706         InputStream is = res.openRawResource(R.drawable.ninepatch_nodpi, value);
707         // This does not call ImageDecoder directly because this entry point is not public.
708         Drawable dr = Drawable.createFromResourceStream(res, value, is, null, null);
709         assertNotNull(dr);
710         assertEquals(5, dr.getIntrinsicWidth());
711         assertEquals(5, dr.getIntrinsicHeight());
712     }
713 
714     @Test
testPostProcessorOverridesNinepatch()715     public void testPostProcessorOverridesNinepatch() {
716         class Listener implements ImageDecoder.OnHeaderDecodedListener {
717             public boolean requireSoftware;
718             @Override
719             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
720                                         ImageDecoder.Source src) {
721                 if (requireSoftware) {
722                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
723                 }
724                 decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN);
725             }
726         };
727         Listener l = new Listener();
728         int resIds[] = new int[] { R.drawable.ninepatch_0,
729                                    R.drawable.ninepatch_1 };
730         boolean trueFalse[] = new boolean[] { true, false };
731         for (int resId : resIds) {
732             for (SourceCreator f : mCreators) {
733                 for (boolean requireSoftware : trueFalse) {
734                     l.requireSoftware = requireSoftware;
735                     ImageDecoder.Source src = f.apply(resId);
736                     try {
737                         Drawable drawable = ImageDecoder.decodeDrawable(src, l);
738                         assertFalse(drawable instanceof NinePatchDrawable);
739 
740                         src = f.apply(resId);
741                         Bitmap bm = ImageDecoder.decodeBitmap(src, l);
742                         assertNull(bm.getNinePatchChunk());
743                     } catch (IOException e) {
744                         fail("Failed with exception " + e);
745                     }
746                 }
747             }
748         }
749     }
750 
751     @Test
testPostProcessorAndMadeOpaque()752     public void testPostProcessorAndMadeOpaque() {
753         class Listener implements ImageDecoder.OnHeaderDecodedListener {
754             public boolean requireSoftware;
755             @Override
756             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
757                                         ImageDecoder.Source src) {
758                 if (requireSoftware) {
759                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
760                 }
761                 decoder.setPostProcessor((c) -> PixelFormat.OPAQUE);
762             }
763         };
764         Listener l = new Listener();
765         boolean trueFalse[] = new boolean[] { true, false };
766         int resIds[] = new int[] { R.drawable.alpha, R.drawable.google_logo_2 };
767         for (int resId : resIds) {
768             for (SourceCreator f : mCreators) {
769                 for (boolean requireSoftware : trueFalse) {
770                     l.requireSoftware = requireSoftware;
771                     ImageDecoder.Source src = f.apply(resId);
772                     try {
773                         Bitmap bm = ImageDecoder.decodeBitmap(src, l);
774                         assertFalse(bm.hasAlpha());
775                         assertFalse(bm.isPremultiplied());
776                     } catch (IOException e) {
777                         fail("Failed with exception " + e);
778                     }
779                 }
780             }
781         }
782     }
783 
784     @Test
785     @Parameters(method = "getRecords")
testPostProcessorAndAddedTransparency(Record record)786     public void testPostProcessorAndAddedTransparency(Record record) {
787         class Listener implements ImageDecoder.OnHeaderDecodedListener {
788             public boolean requireSoftware;
789             @Override
790             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
791                                         ImageDecoder.Source src) {
792                 if (requireSoftware) {
793                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
794                 }
795                 decoder.setPostProcessor((c) -> PixelFormat.TRANSLUCENT);
796             }
797         };
798         Listener l = new Listener();
799         boolean trueFalse[] = new boolean[] { true, false };
800         for (SourceCreator f : mCreators) {
801             for (boolean requireSoftware : trueFalse) {
802                 l.requireSoftware = requireSoftware;
803                 ImageDecoder.Source src = f.apply(record.resId);
804                 try {
805                     Bitmap bm = ImageDecoder.decodeBitmap(src, l);
806                     assertTrue(bm.hasAlpha());
807                     assertTrue(bm.isPremultiplied());
808                 } catch (IOException e) {
809                     fail("Failed with exception " + e);
810                 }
811             }
812         }
813     }
814 
815     @Test(expected = IllegalArgumentException.class)
testPostProcessorTRANSPARENT()816     public void testPostProcessorTRANSPARENT() {
817         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
818         try {
819             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
820                 decoder.setPostProcessor((c) -> PixelFormat.TRANSPARENT);
821             });
822         } catch (IOException e) {
823             fail("Failed with exception " + e);
824         }
825     }
826 
827     @Test(expected = IllegalArgumentException.class)
testPostProcessorInvalidReturn()828     public void testPostProcessorInvalidReturn() {
829         ImageDecoder.Source src = mCreators[0].apply(getRecord().resId);
830         try {
831             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
832                 decoder.setPostProcessor((c) -> 42);
833             });
834         } catch (IOException e) {
835             fail("Failed with exception " + e);
836         }
837     }
838 
839     @Test(expected = IllegalStateException.class)
testPostProcessorAndUnpremul()840     public void testPostProcessorAndUnpremul() {
841         ImageDecoder.Source src = mCreators[0].apply(getRecord().resId);
842         try {
843             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
844                 decoder.setUnpremultipliedRequired(true);
845                 decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN);
846             });
847         } catch (IOException e) {
848             fail("Failed with exception " + e);
849         }
850     }
851 
852     @Test
853     @Parameters(method = "getRecords")
testPostProcessorAndScale(Record record)854     public void testPostProcessorAndScale(Record record) {
855         class PostProcessorWithSize implements PostProcessor {
856             public int width;
857             public int height;
858             @Override
859             public int onPostProcess(Canvas canvas) {
860                 assertEquals(this.width,  width);
861                 assertEquals(this.height, height);
862                 return PixelFormat.UNKNOWN;
863             };
864         };
865         final PostProcessorWithSize pp = new PostProcessorWithSize();
866         pp.width =  record.width  / 2;
867         pp.height = record.height / 2;
868         for (SourceCreator f : mCreators) {
869             ImageDecoder.Source src = f.apply(record.resId);
870             try {
871                 Drawable drawable = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
872                     decoder.setTargetSize(pp.width, pp.height);
873                     decoder.setPostProcessor(pp);
874                 });
875                 assertEquals(pp.width,  drawable.getIntrinsicWidth());
876                 assertEquals(pp.height, drawable.getIntrinsicHeight());
877             } catch (IOException e) {
878                 fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
879             }
880         }
881     }
882 
checkSampleSize(String name, int originalDimension, int sampleSize, int result)883     private void checkSampleSize(String name, int originalDimension, int sampleSize, int result) {
884         if (originalDimension % sampleSize == 0) {
885             assertEquals("Mismatch for " + name + ": " + originalDimension + " / " + sampleSize
886                          + " != " + result, originalDimension / sampleSize, result);
887         } else if (originalDimension <= sampleSize) {
888             assertEquals(1, result);
889         } else {
890             // Rounding may result in differences.
891             int size = result * sampleSize;
892             assertTrue("Rounding mismatch for " + name + ": " + originalDimension + " / "
893                        + sampleSize + " = " + result,
894                        Math.abs(size - originalDimension) < sampleSize);
895         }
896     }
897 
898     @Test
899     @Parameters(method = "getRecords")
testSampleSize(Record record)900     public void testSampleSize(Record record) {
901         final String name = Utils.getAsResourceUri(record.resId).toString();
902         for (int sampleSize : new int[] { 2, 3, 4, 8, 32 }) {
903             ImageDecoder.Source src = mCreators[0].apply(record.resId);
904             try {
905                 Drawable dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
906                     decoder.setTargetSampleSize(sampleSize);
907                 });
908 
909                 checkSampleSize(name, record.width, sampleSize, dr.getIntrinsicWidth());
910                 checkSampleSize(name, record.height, sampleSize, dr.getIntrinsicHeight());
911             } catch (IOException e) {
912                 fail("Failed " + name + " with exception " + e);
913             }
914         }
915     }
916 
917     private interface SampleSizeSupplier extends ToIntFunction<Size> {};
918 
919     @Test
920     @Parameters(method = "getRecords")
testLargeSampleSize(Record record)921     public void testLargeSampleSize(Record record) {
922         ImageDecoder.Source src = mCreators[0].apply(record.resId);
923         for (SampleSizeSupplier supplySampleSize : new SampleSizeSupplier[] {
924                 (size) -> size.getWidth(),
925                 (size) -> size.getWidth() + 5,
926                 (size) -> size.getWidth() * 5,
927         }) {
928             try {
929                 Drawable dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
930                     int sampleSize = supplySampleSize.applyAsInt(info.getSize());
931                     decoder.setTargetSampleSize(sampleSize);
932                 });
933                 assertEquals(1, dr.getIntrinsicWidth());
934             } catch (Exception e) {
935                 String file = Utils.getAsResourceUri(record.resId).toString();
936                 fail("Failed to decode " + file + " with exception " + e);
937             }
938         }
939     }
940 
941     @Test
testResizeTransparency()942     public void testResizeTransparency() {
943         ImageDecoder.Source src = mCreators[0].apply(R.drawable.animated);
944         Drawable dr = null;
945         try {
946             dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
947                 Size size = info.getSize();
948                 decoder.setTargetSize(size.getWidth() - 5, size.getHeight() - 5);
949             });
950         } catch (IOException e) {
951             fail("Failed with exception " + e);
952         }
953 
954         final int width = dr.getIntrinsicWidth();
955         final int height = dr.getIntrinsicHeight();
956 
957         // Draw to a fully transparent Bitmap. Pixels that are transparent in the image will be
958         // transparent.
959         Bitmap normal = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
960         {
961             Canvas canvas = new Canvas(normal);
962             dr.draw(canvas);
963         }
964 
965         // Draw to a BLUE Bitmap. Any pixels that are transparent in the image remain BLUE.
966         Bitmap blended = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
967         {
968             Canvas canvas = new Canvas(blended);
969             canvas.drawColor(Color.BLUE);
970             dr.draw(canvas);
971         }
972 
973         boolean hasTransparency = false;
974         for (int i = 0; i < width; ++i) {
975             for (int j = 0; j < height; ++j) {
976                 int normalColor = normal.getPixel(i, j);
977                 int blendedColor = blended.getPixel(i, j);
978                 if (normalColor == Color.TRANSPARENT) {
979                     hasTransparency = true;
980                     assertEquals(Color.BLUE, blendedColor);
981                 } else if (Color.alpha(normalColor) == 255) {
982                     assertEquals(normalColor, blendedColor);
983                 }
984             }
985         }
986 
987         // Verify that the image has transparency. Otherwise the test is not useful.
988         assertTrue(hasTransparency);
989     }
990 
991     @Test
testGetOnPartialImageListener()992     public void testGetOnPartialImageListener() {
993         OnPartialImageListener[] listeners = new OnPartialImageListener[] {
994                 (e) -> true,
995                 (e) -> false,
996                 null,
997         };
998 
999         final int resId = getRecord().resId;
1000         ImageDecoder.Source src = mCreators[0].apply(resId);
1001         try {
1002             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1003                 assertNull(decoder.getOnPartialImageListener());
1004 
1005                 for (OnPartialImageListener l : listeners) {
1006                     decoder.setOnPartialImageListener(l);
1007                     assertSame(l, decoder.getOnPartialImageListener());
1008                 }
1009             });
1010         } catch (IOException e) {
1011             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
1012         }
1013     }
1014 
1015     @Test
testEarlyIncomplete()1016     public void testEarlyIncomplete() {
1017         byte[] bytes = getAsByteArray(R.raw.basi6a16);
1018         // This is too early to create a partial image, so we throw the Exception
1019         // without calling the listener.
1020         int truncatedLength = 49;
1021         ImageDecoder.Source src = ImageDecoder.createSource(
1022                 ByteBuffer.wrap(bytes, 0, truncatedLength));
1023         try {
1024             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1025                 decoder.setOnPartialImageListener((e) -> {
1026                     fail("No need to call listener; no partial image to display!"
1027                             + " Exception: " + e);
1028                     return false;
1029                 });
1030             });
1031         } catch (DecodeException e) {
1032             assertEquals(DecodeException.SOURCE_INCOMPLETE, e.getError());
1033             assertSame(src, e.getSource());
1034         } catch (IOException ioe) {
1035             fail("Threw some other exception: " + ioe);
1036         }
1037     }
1038 
1039     private class ExceptionStream extends InputStream {
1040         private final InputStream mInputStream;
1041         private final int mExceptionPosition;
1042         int mPosition;
1043 
ExceptionStream(int resId, int exceptionPosition)1044         ExceptionStream(int resId, int exceptionPosition) {
1045             mInputStream = getResources().openRawResource(resId);
1046             mExceptionPosition = exceptionPosition;
1047             mPosition = 0;
1048         }
1049 
1050         @Override
read()1051         public int read() throws IOException {
1052             if (mPosition >= mExceptionPosition) {
1053                 throw new IOException();
1054             }
1055 
1056             int value = mInputStream.read();
1057             mPosition++;
1058             return value;
1059         }
1060 
1061         @Override
read(byte[] b, int off, int len)1062         public int read(byte[] b, int off, int len) throws IOException {
1063             if (mPosition + len <= mExceptionPosition) {
1064                 final int bytesRead = mInputStream.read(b, off, len);
1065                 mPosition += bytesRead;
1066                 return bytesRead;
1067             }
1068 
1069             len = mExceptionPosition - mPosition;
1070             mPosition += mInputStream.read(b, off, len);
1071             throw new IOException();
1072         }
1073     }
1074 
1075     @Test
testExceptionInStream()1076     public void testExceptionInStream() throws Throwable {
1077         InputStream is = new ExceptionStream(R.drawable.animated, 27570);
1078         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), is,
1079                 Bitmap.DENSITY_NONE);
1080         Drawable dr = null;
1081         try {
1082             dr = ImageDecoder.decodeDrawable(src);
1083             fail("Expected to throw an exception!");
1084         } catch (IOException ioe) {
1085             assertTrue(ioe instanceof DecodeException);
1086             DecodeException decodeException = (DecodeException) ioe;
1087             assertEquals(DecodeException.SOURCE_EXCEPTION, decodeException.getError());
1088             Throwable throwable = decodeException.getCause();
1089             assertNotNull(throwable);
1090             assertTrue(throwable instanceof IOException);
1091         }
1092         assertNull(dr);
1093     }
1094 
1095     @Test
1096     @Parameters(method = "getRecords")
testOnPartialImage(Record record)1097     public void testOnPartialImage(Record record) {
1098         class PartialImageCallback implements OnPartialImageListener {
1099             public boolean wasCalled;
1100             public boolean returnDrawable;
1101             public ImageDecoder.Source source;
1102             @Override
1103             public boolean onPartialImage(DecodeException e) {
1104                 wasCalled = true;
1105                 assertEquals(DecodeException.SOURCE_INCOMPLETE, e.getError());
1106                 assertSame(source, e.getSource());
1107                 return returnDrawable;
1108             }
1109         };
1110         final PartialImageCallback callback = new PartialImageCallback();
1111         boolean abortDecode[] = new boolean[] { true, false };
1112         byte[] bytes = getAsByteArray(record.resId);
1113         int truncatedLength = bytes.length / 2;
1114         if (record.mimeType.equals("image/x-ico")
1115                 || record.mimeType.equals("image/x-adobe-dng")
1116                 || record.mimeType.equals("image/heif")
1117                 || record.mimeType.equals("image/avif")) {
1118             // FIXME (scroggo): Some codecs currently do not support incomplete images.
1119             return;
1120         }
1121         if (record.resId == R.drawable.grayscale_jpg) {
1122             // FIXME (scroggo): This is a progressive jpeg. If Skia switches to
1123             // decoding jpegs progressively, this image can be partially decoded.
1124             return;
1125         }
1126         for (boolean abort : abortDecode) {
1127             ImageDecoder.Source src = ImageDecoder.createSource(
1128                     ByteBuffer.wrap(bytes, 0, truncatedLength));
1129             callback.wasCalled = false;
1130             callback.returnDrawable = !abort;
1131             callback.source = src;
1132             try {
1133                 Drawable drawable = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1134                     decoder.setOnPartialImageListener(callback);
1135                 });
1136                 assertFalse(abort);
1137                 assertNotNull(drawable);
1138                 assertEquals(record.width,  drawable.getIntrinsicWidth());
1139                 assertEquals(record.height, drawable.getIntrinsicHeight());
1140             } catch (IOException e) {
1141                 assertTrue(abort);
1142             }
1143             assertTrue(callback.wasCalled);
1144         }
1145 
1146         // null listener behaves as if onPartialImage returned false.
1147         ImageDecoder.Source src = ImageDecoder.createSource(
1148                 ByteBuffer.wrap(bytes, 0, truncatedLength));
1149         try {
1150             ImageDecoder.decodeDrawable(src);
1151             fail("Should have thrown an exception!");
1152         } catch (DecodeException incomplete) {
1153             // This is the correct behavior.
1154         } catch (IOException e) {
1155             fail("Failed with exception " + e);
1156         }
1157     }
1158 
1159     @Test
testCorruptException()1160     public void testCorruptException() {
1161         class PartialImageCallback implements OnPartialImageListener {
1162             public boolean wasCalled = false;
1163             public ImageDecoder.Source source;
1164             @Override
1165             public boolean onPartialImage(DecodeException e) {
1166                 wasCalled = true;
1167                 assertEquals(DecodeException.SOURCE_MALFORMED_DATA, e.getError());
1168                 assertSame(source, e.getSource());
1169                 return true;
1170             }
1171         };
1172         final PartialImageCallback callback = new PartialImageCallback();
1173         byte[] bytes = getAsByteArray(R.drawable.png_test);
1174         // The four bytes starting with byte 40,000 represent the CRC. Changing
1175         // them will cause the decode to fail.
1176         for (int i = 0; i < 4; ++i) {
1177             bytes[40000 + i] = 'X';
1178         }
1179         ImageDecoder.Source src = ImageDecoder.createSource(ByteBuffer.wrap(bytes));
1180         callback.wasCalled = false;
1181         callback.source = src;
1182         try {
1183             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1184                 decoder.setOnPartialImageListener(callback);
1185             });
1186         } catch (IOException e) {
1187             fail("Failed with exception " + e);
1188         }
1189         assertTrue(callback.wasCalled);
1190     }
1191 
1192     private static class DummyException extends RuntimeException {};
1193 
1194     @Test
testPartialImageThrowException()1195     public void  testPartialImageThrowException() {
1196         byte[] bytes = getAsByteArray(R.drawable.png_test);
1197         ImageDecoder.Source src = ImageDecoder.createSource(
1198                 ByteBuffer.wrap(bytes, 0, bytes.length / 2));
1199         try {
1200             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1201                 decoder.setOnPartialImageListener((e) -> {
1202                     throw new DummyException();
1203                 });
1204             });
1205             fail("Should have thrown an exception");
1206         } catch (DummyException dummy) {
1207             // This is correct.
1208         } catch (Throwable t) {
1209             fail("Should have thrown DummyException - threw " + t + " instead");
1210         }
1211     }
1212 
1213     @Test
testGetMutable()1214     public void testGetMutable() {
1215         final int resId = getRecord().resId;
1216         ImageDecoder.Source src = mCreators[0].apply(resId);
1217         try {
1218             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1219                 assertFalse(decoder.isMutableRequired());
1220 
1221                 decoder.setMutableRequired(true);
1222                 assertTrue(decoder.isMutableRequired());
1223 
1224                 decoder.setMutableRequired(false);
1225                 assertFalse(decoder.isMutableRequired());
1226             });
1227         } catch (IOException e) {
1228             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
1229         }
1230     }
1231 
1232     @Test
1233     @Parameters(method = "getRecords")
testMutable(Record record)1234     public void testMutable(Record record) {
1235         int allocators[] = new int[] { ImageDecoder.ALLOCATOR_DEFAULT,
1236                                        ImageDecoder.ALLOCATOR_SOFTWARE,
1237                                        ImageDecoder.ALLOCATOR_SHARED_MEMORY };
1238         class HeaderListener implements ImageDecoder.OnHeaderDecodedListener {
1239             int allocator;
1240             boolean postProcess;
1241             @Override
1242             public void onHeaderDecoded(ImageDecoder decoder,
1243                                         ImageDecoder.ImageInfo info,
1244                                         ImageDecoder.Source src) {
1245                 decoder.setMutableRequired(true);
1246                 decoder.setAllocator(allocator);
1247                 if (postProcess) {
1248                     decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN);
1249                 }
1250             }
1251         };
1252         HeaderListener l = new HeaderListener();
1253         boolean trueFalse[] = new boolean[] { true, false };
1254         ImageDecoder.Source src = mCreators[0].apply(record.resId);
1255         for (boolean postProcess : trueFalse) {
1256             for (int allocator : allocators) {
1257                 l.allocator = allocator;
1258                 l.postProcess = postProcess;
1259 
1260                 try {
1261                     Bitmap bm = ImageDecoder.decodeBitmap(src, l);
1262                     assertTrue(bm.isMutable());
1263                     assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
1264                 } catch (Exception e) {
1265                     String file = Utils.getAsResourceUri(record.resId).toString();
1266                     fail("Failed to decode " + file + " with exception " + e);
1267                 }
1268             }
1269         }
1270     }
1271 
1272     @Test(expected = IllegalStateException.class)
testMutableHardware()1273     public void testMutableHardware() {
1274         ImageDecoder.Source src = mCreators[0].apply(getRecord().resId);
1275         try {
1276             ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
1277                 decoder.setMutableRequired(true);
1278                 decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE);
1279             });
1280         } catch (IOException e) {
1281             fail("Failed with exception " + e);
1282         }
1283     }
1284 
1285     @Test(expected = IllegalStateException.class)
testMutableDrawable()1286     public void testMutableDrawable() {
1287         ImageDecoder.Source src = mCreators[0].apply(getRecord().resId);
1288         try {
1289             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1290                 decoder.setMutableRequired(true);
1291             });
1292         } catch (IOException e) {
1293             fail("Failed with exception " + e);
1294         }
1295     }
1296 
1297     private interface EmptyByteBufferCreator {
apply()1298         public ByteBuffer apply();
1299     };
1300 
1301     @Test
testEmptyByteBuffer()1302     public void testEmptyByteBuffer() {
1303         class Direct implements EmptyByteBufferCreator {
1304             @Override
1305             public ByteBuffer apply() {
1306                 return ByteBuffer.allocateDirect(0);
1307             }
1308         };
1309         class Wrap implements EmptyByteBufferCreator {
1310             @Override
1311             public ByteBuffer apply() {
1312                 byte[] bytes = new byte[0];
1313                 return ByteBuffer.wrap(bytes);
1314             }
1315         };
1316         class ReadOnly implements EmptyByteBufferCreator {
1317             @Override
1318             public ByteBuffer apply() {
1319                 byte[] bytes = new byte[0];
1320                 return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
1321             }
1322         };
1323         EmptyByteBufferCreator creators[] = new EmptyByteBufferCreator[] {
1324             new Direct(), new Wrap(), new ReadOnly() };
1325         for (EmptyByteBufferCreator creator : creators) {
1326             try {
1327                 ImageDecoder.decodeDrawable(
1328                         ImageDecoder.createSource(creator.apply()));
1329                 fail("This should have thrown an exception");
1330             } catch (IOException e) {
1331                 // This is correct.
1332             }
1333         }
1334     }
1335 
1336     @Test(expected = IllegalArgumentException.class)
testZeroSampleSize()1337     public void testZeroSampleSize() {
1338         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1339         try {
1340             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> decoder.setTargetSampleSize(0));
1341         } catch (IOException e) {
1342             fail("Failed with exception " + e);
1343         }
1344     }
1345 
1346     @Test(expected = IllegalArgumentException.class)
testNegativeSampleSize()1347     public void testNegativeSampleSize() {
1348         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1349         try {
1350             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> decoder.setTargetSampleSize(-2));
1351         } catch (IOException e) {
1352             fail("Failed with exception " + e);
1353         }
1354     }
1355 
1356     @Test
1357     @Parameters(method = "getRecords")
testTargetSize(Record record)1358     public void testTargetSize(Record record) {
1359         class ResizeListener implements ImageDecoder.OnHeaderDecodedListener {
1360             public int width;
1361             public int height;
1362             @Override
1363             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1364                                         ImageDecoder.Source src) {
1365                 decoder.setTargetSize(width, height);
1366             }
1367         };
1368         ResizeListener l = new ResizeListener();
1369 
1370         float[] scales = new float[] { .0625f, .125f, .25f, .5f, .75f, 1.1f, 2.0f };
1371         ImageDecoder.Source src = mCreators[0].apply(record.resId);
1372         for (int j = 0; j < scales.length; ++j) {
1373             l.width  = (int) (scales[j] * record.width);
1374             l.height = (int) (scales[j] * record.height);
1375 
1376             try {
1377                 Drawable drawable = ImageDecoder.decodeDrawable(src, l);
1378                 assertEquals(l.width,  drawable.getIntrinsicWidth());
1379                 assertEquals(l.height, drawable.getIntrinsicHeight());
1380 
1381                 Bitmap bm = ImageDecoder.decodeBitmap(src, l);
1382                 assertEquals(l.width,  bm.getWidth());
1383                 assertEquals(l.height, bm.getHeight());
1384             } catch (IOException e) {
1385                 fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
1386             }
1387         }
1388 
1389         try {
1390             // Arbitrary square.
1391             l.width  = 50;
1392             l.height = 50;
1393             Drawable drawable = ImageDecoder.decodeDrawable(src, l);
1394             assertEquals(50,  drawable.getIntrinsicWidth());
1395             assertEquals(50, drawable.getIntrinsicHeight());
1396 
1397             // Swap width and height, for different scales.
1398             l.height = record.width;
1399             l.width  = record.height;
1400             drawable = ImageDecoder.decodeDrawable(src, l);
1401             assertEquals(record.height, drawable.getIntrinsicWidth());
1402             assertEquals(record.width,  drawable.getIntrinsicHeight());
1403         } catch (IOException e) {
1404             fail("Failed with exception " + e);
1405         }
1406     }
1407 
1408     @Test
testResizeWebp()1409     public void testResizeWebp() {
1410         // libwebp supports unpremultiplied for downscaled output
1411         class ResizeListener implements ImageDecoder.OnHeaderDecodedListener {
1412             public int width;
1413             public int height;
1414             @Override
1415             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1416                                         ImageDecoder.Source src) {
1417                 decoder.setTargetSize(width, height);
1418                 decoder.setUnpremultipliedRequired(true);
1419             }
1420         };
1421         ResizeListener l = new ResizeListener();
1422 
1423         float[] scales = new float[] { .0625f, .125f, .25f, .5f, .75f };
1424         for (SourceCreator f : mCreators) {
1425             for (int j = 0; j < scales.length; ++j) {
1426                 l.width  = (int) (scales[j] * 240);
1427                 l.height = (int) (scales[j] *  87);
1428 
1429                 ImageDecoder.Source src = f.apply(R.drawable.google_logo_2);
1430                 try {
1431                     Bitmap bm = ImageDecoder.decodeBitmap(src, l);
1432                     assertEquals(l.width,  bm.getWidth());
1433                     assertEquals(l.height, bm.getHeight());
1434                     assertTrue(bm.hasAlpha());
1435                     assertFalse(bm.isPremultiplied());
1436                 } catch (IOException e) {
1437                     fail("Failed with exception " + e);
1438                 }
1439             }
1440         }
1441     }
1442 
1443     @Test(expected = IllegalStateException.class)
testResizeWebpLarger()1444     public void testResizeWebpLarger() {
1445         // libwebp does not upscale, so there is no way to get unpremul.
1446         ImageDecoder.Source src = mCreators[0].apply(R.drawable.google_logo_2);
1447         try {
1448             ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
1449                 Size size = info.getSize();
1450                 decoder.setTargetSize(size.getWidth() * 2, size.getHeight() * 2);
1451                 decoder.setUnpremultipliedRequired(true);
1452             });
1453         } catch (IOException e) {
1454             fail("Failed with exception " + e);
1455         }
1456     }
1457 
1458     @Test(expected = IllegalStateException.class)
testResizeUnpremul()1459     public void testResizeUnpremul() {
1460         ImageDecoder.Source src = mCreators[0].apply(R.drawable.alpha);
1461         try {
1462             ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
1463                 // Choose a width and height that cannot be achieved with sampling.
1464                 Size size = info.getSize();
1465                 int width = size.getWidth() / 2 + 3;
1466                 int height = size.getHeight() / 2 + 3;
1467                 decoder.setTargetSize(width, height);
1468                 decoder.setUnpremultipliedRequired(true);
1469             });
1470         } catch (IOException e) {
1471             fail("Failed with exception " + e);
1472         }
1473     }
1474 
1475     @Test
testGetCrop()1476     public void testGetCrop() {
1477         final int resId = getRecord().resId;
1478         ImageDecoder.Source src = mCreators[0].apply(resId);
1479         try {
1480             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1481                 assertNull(decoder.getCrop());
1482 
1483                 Rect r = new Rect(0, 0, info.getSize().getWidth() / 2, 5);
1484                 decoder.setCrop(r);
1485                 assertEquals(r, decoder.getCrop());
1486 
1487                 r = new Rect(0, 0, 5, 10);
1488                 decoder.setCrop(r);
1489                 assertEquals(r, decoder.getCrop());
1490             });
1491         } catch (IOException e) {
1492             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
1493         }
1494     }
1495 
1496     @Test
1497     @Parameters(method = "getRecords")
testCrop(Record record)1498     public void testCrop(Record record) {
1499         class Listener implements ImageDecoder.OnHeaderDecodedListener {
1500             public boolean doScale;
1501             public boolean requireSoftware;
1502             public Rect cropRect;
1503             @Override
1504             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1505                                         ImageDecoder.Source src) {
1506                 int width  = info.getSize().getWidth();
1507                 int height = info.getSize().getHeight();
1508                 if (doScale) {
1509                     width  /= 2;
1510                     height /= 2;
1511                     decoder.setTargetSize(width, height);
1512                 }
1513                 // Crop to the middle:
1514                 int quarterWidth  = width  / 4;
1515                 int quarterHeight = height / 4;
1516                 cropRect = new Rect(quarterWidth, quarterHeight,
1517                         quarterWidth * 3, quarterHeight * 3);
1518                 decoder.setCrop(cropRect);
1519 
1520                 if (requireSoftware) {
1521                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
1522                 }
1523             }
1524         };
1525         Listener l = new Listener();
1526         boolean trueFalse[] = new boolean[] { true, false };
1527         for (SourceCreator f : mCreators) {
1528             for (boolean doScale : trueFalse) {
1529                 l.doScale = doScale;
1530                 for (boolean requireSoftware : trueFalse) {
1531                     l.requireSoftware = requireSoftware;
1532                     ImageDecoder.Source src = f.apply(record.resId);
1533 
1534                     try {
1535                         Drawable drawable = ImageDecoder.decodeDrawable(src, l);
1536                         assertEquals(l.cropRect.width(),  drawable.getIntrinsicWidth());
1537                         assertEquals(l.cropRect.height(), drawable.getIntrinsicHeight());
1538                     } catch (IOException e) {
1539                         fail("Failed " + Utils.getAsResourceUri(record.resId)
1540                                 + " with exception " + e);
1541                     }
1542                 }
1543             }
1544         }
1545     }
1546 
1547     @Test
testScaleAndCrop()1548     public void testScaleAndCrop() {
1549         class CropListener implements ImageDecoder.OnHeaderDecodedListener {
1550             public boolean doCrop = true;
1551             public Rect outScaledRect = null;
1552             public Rect outCropRect = null;
1553 
1554             @Override
1555             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1556                                         ImageDecoder.Source src) {
1557                 // Use software for pixel comparison.
1558                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
1559 
1560                 // Scale to a size that is not directly supported by sampling.
1561                 Size originalSize = info.getSize();
1562                 int scaledWidth = originalSize.getWidth() * 2 / 3;
1563                 int scaledHeight = originalSize.getHeight() * 2 / 3;
1564                 decoder.setTargetSize(scaledWidth, scaledHeight);
1565 
1566                 outScaledRect = new Rect(0, 0, scaledWidth, scaledHeight);
1567 
1568                 if (doCrop) {
1569                     outCropRect = new Rect(scaledWidth / 2, scaledHeight / 2,
1570                             scaledWidth, scaledHeight);
1571                     decoder.setCrop(outCropRect);
1572                 }
1573             }
1574         }
1575         CropListener l = new CropListener();
1576         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1577 
1578         // Scale and crop in a single step.
1579         Bitmap oneStepBm = null;
1580         try {
1581             oneStepBm = ImageDecoder.decodeBitmap(src, l);
1582         } catch (IOException e) {
1583             fail("Failed with exception " + e);
1584         }
1585         assertNotNull(oneStepBm);
1586         assertNotNull(l.outCropRect);
1587         assertEquals(l.outCropRect.width(), oneStepBm.getWidth());
1588         assertEquals(l.outCropRect.height(), oneStepBm.getHeight());
1589         Rect cropRect = new Rect(l.outCropRect);
1590 
1591         assertNotNull(l.outScaledRect);
1592         Rect scaledRect = new Rect(l.outScaledRect);
1593 
1594         // Now just scale with ImageDecoder, and crop afterwards.
1595         l.doCrop = false;
1596         Bitmap twoStepBm = null;
1597         try {
1598             twoStepBm = ImageDecoder.decodeBitmap(src, l);
1599         } catch (IOException e) {
1600             fail("Failed with exception " + e);
1601         }
1602         assertNotNull(twoStepBm);
1603         assertEquals(scaledRect.width(), twoStepBm.getWidth());
1604         assertEquals(scaledRect.height(), twoStepBm.getHeight());
1605 
1606         Bitmap cropped = Bitmap.createBitmap(twoStepBm, cropRect.left, cropRect.top,
1607                 cropRect.width(), cropRect.height());
1608         assertNotNull(cropped);
1609 
1610         // The two should look the same.
1611         assertTrue(BitmapUtils.compareBitmaps(cropped, oneStepBm, .99));
1612     }
1613 
1614     @Test(expected = IllegalArgumentException.class)
testResizeZeroX()1615     public void testResizeZeroX() {
1616         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1617         try {
1618             ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
1619                     decoder.setTargetSize(0, info.getSize().getHeight()));
1620         } catch (IOException e) {
1621             fail("Failed with exception " + e);
1622         }
1623     }
1624 
1625     @Test(expected = IllegalArgumentException.class)
testResizeZeroY()1626     public void testResizeZeroY() {
1627         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1628         try {
1629             ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
1630                     decoder.setTargetSize(info.getSize().getWidth(), 0));
1631         } catch (IOException e) {
1632             fail("Failed with exception " + e);
1633         }
1634     }
1635 
1636     @Test(expected = IllegalArgumentException.class)
testResizeNegativeX()1637     public void testResizeNegativeX() {
1638         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1639         try {
1640             ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
1641                     decoder.setTargetSize(-10, info.getSize().getHeight()));
1642         } catch (IOException e) {
1643             fail("Failed with exception " + e);
1644         }
1645     }
1646 
1647     @Test(expected = IllegalArgumentException.class)
testResizeNegativeY()1648     public void testResizeNegativeY() {
1649         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1650         try {
1651             ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
1652                     decoder.setTargetSize(info.getSize().getWidth(), -10));
1653         } catch (IOException e) {
1654             fail("Failed with exception " + e);
1655         }
1656     }
1657 
1658     @Test(expected = IllegalStateException.class)
testStoreImageDecoder()1659     public void testStoreImageDecoder() {
1660         class CachingCallback implements ImageDecoder.OnHeaderDecodedListener {
1661             ImageDecoder cachedDecoder;
1662             @Override
1663             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1664                                         ImageDecoder.Source src) {
1665                 cachedDecoder = decoder;
1666             }
1667         };
1668         CachingCallback l = new CachingCallback();
1669         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1670         try {
1671             ImageDecoder.decodeDrawable(src, l);
1672         } catch (IOException e) {
1673             fail("Failed with exception " + e);
1674         }
1675         l.cachedDecoder.setTargetSampleSize(2);
1676     }
1677 
1678     @Test(expected = IllegalStateException.class)
testDecodeUnpremulDrawable()1679     public void testDecodeUnpremulDrawable() {
1680         ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
1681         try {
1682             ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
1683                     decoder.setUnpremultipliedRequired(true));
1684         } catch (IOException e) {
1685             fail("Failed with exception " + e);
1686         }
1687     }
1688 
1689     // One static PNG and one animated GIF to test setting invalid crop rects,
1690     // to test both paths (animated and non-animated) through ImageDecoder.
resourcesForCropTests()1691     private static Object[] resourcesForCropTests() {
1692         return new Object[] { R.drawable.png_test, R.drawable.animated };
1693     }
1694 
1695     @Test(expected = IllegalStateException.class)
1696     @Parameters(method = "resourcesForCropTests")
testInvertCropWidth(int resId)1697     public void testInvertCropWidth(int resId) {
1698         ImageDecoder.Source src = mCreators[0].apply(resId);
1699         try {
1700             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1701                 // This rect is unsorted.
1702                 decoder.setCrop(new Rect(info.getSize().getWidth(), 0, 0,
1703                                          info.getSize().getHeight()));
1704             });
1705         } catch (IOException e) {
1706             fail("Failed with exception " + e);
1707         }
1708     }
1709 
1710     @Test(expected = IllegalStateException.class)
1711     @Parameters(method = "resourcesForCropTests")
testInvertCropHeight(int resId)1712     public void testInvertCropHeight(int resId) {
1713         ImageDecoder.Source src = mCreators[0].apply(resId);
1714         try {
1715             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1716                 // This rect is unsorted.
1717                 decoder.setCrop(new Rect(0, info.getSize().getWidth(),
1718                                          info.getSize().getHeight(), 0));
1719             });
1720         } catch (IOException e) {
1721             fail("Failed with exception " + e);
1722         }
1723     }
1724 
1725     @Test(expected = IllegalStateException.class)
1726     @Parameters(method = "resourcesForCropTests")
testEmptyCrop(int resId)1727     public void testEmptyCrop(int resId) {
1728         ImageDecoder.Source src = mCreators[0].apply(resId);
1729         try {
1730             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1731                 decoder.setCrop(new Rect(1, 1, 1, 1));
1732             });
1733         } catch (IOException e) {
1734             fail("Failed with exception " + e);
1735         }
1736     }
1737 
1738     @Test(expected = IllegalStateException.class)
1739     @Parameters(method = "resourcesForCropTests")
testCropNegativeLeft(int resId)1740     public void testCropNegativeLeft(int resId) {
1741         ImageDecoder.Source src = mCreators[0].apply(resId);
1742         try {
1743             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1744                 decoder.setCrop(new Rect(-1, 0, info.getSize().getWidth(),
1745                                                 info.getSize().getHeight()));
1746             });
1747         } catch (IOException e) {
1748             fail("Failed with exception " + e);
1749         }
1750     }
1751 
1752     @Test(expected = IllegalStateException.class)
1753     @Parameters(method = "resourcesForCropTests")
testCropNegativeTop(int resId)1754     public void testCropNegativeTop(int resId) {
1755         ImageDecoder.Source src = mCreators[0].apply(resId);
1756         try {
1757             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1758                 decoder.setCrop(new Rect(0, -1, info.getSize().getWidth(),
1759                                                 info.getSize().getHeight()));
1760             });
1761         } catch (IOException e) {
1762             fail("Failed with exception " + e);
1763         }
1764     }
1765 
1766     @Test(expected = IllegalStateException.class)
1767     @Parameters(method = "resourcesForCropTests")
testCropTooWide(int resId)1768     public void testCropTooWide(int resId) {
1769         ImageDecoder.Source src = mCreators[0].apply(resId);
1770         try {
1771             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1772                 decoder.setCrop(new Rect(1, 0, info.getSize().getWidth() + 1,
1773                                                info.getSize().getHeight()));
1774             });
1775         } catch (IOException e) {
1776             fail("Failed with exception " + e);
1777         }
1778     }
1779 
1780 
1781     @Test(expected = IllegalStateException.class)
1782     @Parameters(method = "resourcesForCropTests")
testCropTooTall(int resId)1783     public void testCropTooTall(int resId) {
1784         ImageDecoder.Source src = mCreators[0].apply(resId);
1785         try {
1786             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1787                 decoder.setCrop(new Rect(0, 1, info.getSize().getWidth(),
1788                                                info.getSize().getHeight() + 1));
1789             });
1790         } catch (IOException e) {
1791             fail("Failed with exception " + e);
1792         }
1793     }
1794 
1795     @Test(expected = IllegalStateException.class)
1796     @Parameters(method = "resourcesForCropTests")
testCropResize(int resId)1797     public void testCropResize(int resId) {
1798         ImageDecoder.Source src = mCreators[0].apply(resId);
1799         try {
1800             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1801                 Size size = info.getSize();
1802                 decoder.setTargetSize(size.getWidth() / 2, size.getHeight() / 2);
1803                 decoder.setCrop(new Rect(0, 0, size.getWidth(),
1804                                                size.getHeight()));
1805             });
1806         } catch (IOException e) {
1807             fail("Failed with exception " + e);
1808         }
1809     }
1810 
1811     @Test
testAlphaMaskNonGray()1812     public void testAlphaMaskNonGray() {
1813         // It is safe to call setDecodeAsAlphaMaskEnabled on a non-gray image.
1814         SourceCreator f = mCreators[0];
1815         ImageDecoder.Source src = f.apply(R.drawable.png_test);
1816         assertNotNull(src);
1817         try {
1818             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
1819                 decoder.setDecodeAsAlphaMaskEnabled(true);
1820                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
1821             });
1822             assertNotNull(bm);
1823             assertNotEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
1824         } catch (IOException e) {
1825             fail("Failed with exception " + e);
1826         }
1827     }
1828 
1829     @Test
testAlphaPlusSetTargetColorSpace()1830     public void testAlphaPlusSetTargetColorSpace() {
1831         // TargetColorSpace is ignored for ALPHA_8
1832         ImageDecoder.Source src = mCreators[0].apply(R.drawable.grayscale_png);
1833         for (ColorSpace cs : BitmapTest.getRgbColorSpaces()) {
1834             try {
1835                 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
1836                     decoder.setDecodeAsAlphaMaskEnabled(true);
1837                     decoder.setTargetColorSpace(cs);
1838                 });
1839                 assertNotNull(bm);
1840                 assertEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
1841                 assertNull(bm.getColorSpace());
1842             } catch (IOException e) {
1843                 fail("Failed with exception " + e);
1844             }
1845         }
1846     }
1847 
1848     @Test(expected = IllegalStateException.class)
testAlphaMaskPlusHardware()1849     public void testAlphaMaskPlusHardware() {
1850         SourceCreator f = mCreators[0];
1851         ImageDecoder.Source src = f.apply(R.drawable.png_test);
1852         assertNotNull(src);
1853         try {
1854             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1855                 decoder.setDecodeAsAlphaMaskEnabled(true);
1856                 decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE);
1857             });
1858         } catch (IOException e) {
1859             fail("Failed with exception " + e);
1860         }
1861     }
1862 
1863     @Test
testAlphaMaskPlusHardwareAnimated()1864     public void testAlphaMaskPlusHardwareAnimated() {
1865         // AnimatedImageDrawable ignores both of these settings, so it is okay
1866         // to combine them.
1867         SourceCreator f = mCreators[0];
1868         ImageDecoder.Source src = f.apply(R.drawable.animated);
1869         assertNotNull(src);
1870         try {
1871             Drawable d = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1872                 decoder.setDecodeAsAlphaMaskEnabled(true);
1873                 decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE);
1874             });
1875             assertNotNull(d);
1876         } catch (IOException e) {
1877             fail("Failed with exception " + e);
1878         }
1879     }
1880 
1881     @Test
testGetAlphaMask()1882     public void testGetAlphaMask() {
1883         final int resId = R.drawable.grayscale_png;
1884         ImageDecoder.Source src = mCreators[0].apply(resId);
1885         try {
1886             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1887                 assertFalse(decoder.isDecodeAsAlphaMaskEnabled());
1888 
1889                 decoder.setDecodeAsAlphaMaskEnabled(true);
1890                 assertTrue(decoder.isDecodeAsAlphaMaskEnabled());
1891 
1892                 decoder.setDecodeAsAlphaMaskEnabled(false);
1893                 assertFalse(decoder.isDecodeAsAlphaMaskEnabled());
1894             });
1895         } catch (IOException e) {
1896             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
1897         }
1898     }
1899 
1900     @Test
testAlphaMask()1901     public void testAlphaMask() {
1902         class Listener implements ImageDecoder.OnHeaderDecodedListener {
1903             boolean doCrop;
1904             boolean doScale;
1905             boolean doPostProcess;
1906             @Override
1907             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1908                                         ImageDecoder.Source src) {
1909                 decoder.setDecodeAsAlphaMaskEnabled(true);
1910                 Size size = info.getSize();
1911                 if (doScale) {
1912                     decoder.setTargetSize(size.getWidth() / 2,
1913                                           size.getHeight() / 2);
1914                 }
1915                 if (doCrop) {
1916                     decoder.setCrop(new Rect(0, 0, size.getWidth() / 4,
1917                                                    size.getHeight() / 4));
1918                 }
1919                 if (doPostProcess) {
1920                     decoder.setPostProcessor((c) -> {
1921                         c.drawColor(Color.BLACK);
1922                         return PixelFormat.UNKNOWN;
1923                     });
1924                 }
1925             }
1926         };
1927         Listener l = new Listener();
1928         // Both of these are encoded as single channel gray images.
1929         int resIds[] = new int[] { R.drawable.grayscale_png, R.drawable.grayscale_jpg };
1930         boolean trueFalse[] = new boolean[] { true, false };
1931         SourceCreator f = mCreators[0];
1932         for (int resId : resIds) {
1933             // By default, this will decode to HARDWARE
1934             ImageDecoder.Source src = f.apply(resId);
1935             try {
1936                 Bitmap bm = ImageDecoder.decodeBitmap(src);
1937                 assertEquals(Bitmap.Config.HARDWARE, bm.getConfig());
1938             } catch (IOException e) {
1939                 fail("Failed with exception " + e);
1940             }
1941 
1942             // Now set alpha mask, which is incompatible with HARDWARE
1943             for (boolean doCrop : trueFalse) {
1944                 for (boolean doScale : trueFalse) {
1945                     for (boolean doPostProcess : trueFalse) {
1946                         l.doCrop = doCrop;
1947                         l.doScale = doScale;
1948                         l.doPostProcess = doPostProcess;
1949                         src = f.apply(resId);
1950                         try {
1951                             Bitmap bm = ImageDecoder.decodeBitmap(src, l);
1952                             assertEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
1953                             assertNull(bm.getColorSpace());
1954                         } catch (IOException e) {
1955                             fail("Failed with exception " + e);
1956                         }
1957                     }
1958                 }
1959             }
1960         }
1961     }
1962 
1963     @Test
testGetConserveMemory()1964     public void testGetConserveMemory() {
1965         final int resId = getRecord().resId;
1966         ImageDecoder.Source src = mCreators[0].apply(resId);
1967         try {
1968             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
1969                 assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy());
1970 
1971                 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
1972                 assertEquals(ImageDecoder.MEMORY_POLICY_LOW_RAM, decoder.getMemorySizePolicy());
1973 
1974                 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_DEFAULT);
1975                 assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy());
1976             });
1977         } catch (IOException e) {
1978             fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
1979         }
1980     }
1981 
1982     @Test
testConserveMemoryPlusHardware()1983     public void testConserveMemoryPlusHardware() {
1984         class Listener implements ImageDecoder.OnHeaderDecodedListener {
1985             int allocator;
1986             @Override
1987             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
1988                                         ImageDecoder.Source src) {
1989                 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
1990                 decoder.setAllocator(allocator);
1991             }
1992         };
1993         Listener l = new Listener();
1994         SourceCreator f = mCreators[0];
1995         for (int resId : new int[] { R.drawable.png_test, R.raw.f16 }) {
1996             Bitmap normal = null;
1997             try {
1998                 normal = ImageDecoder.decodeBitmap(f.apply(resId), ((decoder, info, source) -> {
1999                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2000                 }));
2001             } catch (IOException e) {
2002                 fail("Failed with exception " + e);
2003             }
2004             assertNotNull(normal);
2005             int normalByteCount = normal.getAllocationByteCount();
2006             int[] allocators = { ImageDecoder.ALLOCATOR_HARDWARE, ImageDecoder.ALLOCATOR_DEFAULT };
2007             for (int allocator : allocators) {
2008                 l.allocator = allocator;
2009                 Bitmap test = null;
2010                 try {
2011                     test = ImageDecoder.decodeBitmap(f.apply(resId), l);
2012                 } catch (IOException e) {
2013                     fail("Failed with exception " + e);
2014                 }
2015                 assertNotNull(test);
2016                 int byteCount = test.getAllocationByteCount();
2017 
2018                 if (resId == R.drawable.png_test) {
2019                     // We do not support 565 in HARDWARE, so no RAM savings
2020                     // are possible.
2021                     // Provide a little wiggle room to allow for gralloc allocation size
2022                     // variances
2023                     assertTrue(byteCount < (normalByteCount * 1.1));
2024                     assertTrue(byteCount >= (normalByteCount * 0.9));
2025                 } else { // R.raw.f16
2026                     // This image defaults to F16. MEMORY_POLICY_LOW_RAM
2027                     // forces "test" to decode to 8888.
2028                     assertTrue(byteCount < normalByteCount);
2029                 }
2030             }
2031         }
2032     }
2033 
2034     @Test
2035     public void testConserveMemory() {
2036         class Listener implements ImageDecoder.OnHeaderDecodedListener {
2037             boolean doPostProcess;
2038             boolean preferRamOverQuality;
2039             @Override
2040             public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
2041                                         ImageDecoder.Source src) {
2042                 if (preferRamOverQuality) {
2043                     decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
2044                 }
2045                 if (doPostProcess) {
2046                     decoder.setPostProcessor((c) -> {
2047                         c.drawColor(Color.BLACK);
2048                         return PixelFormat.TRANSLUCENT;
2049                     });
2050                 }
2051                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2052             }
2053         };
2054         Listener l = new Listener();
2055         // All of these images are opaque, so they can save RAM with
2056         // setConserveMemory.
2057         int resIds[] = new int[] { R.drawable.png_test, R.drawable.baseline_jpeg,
2058                                    // If this were stored in drawable/, it would
2059                                    // be converted from 16-bit to 8. FIXME: Is
2060                                    // behavior still desirable now that we have
2061                                    // F16? b/119760146
2062                                    R.raw.f16 };
2063         // An opaque image can be converted to 565, but postProcess will promote
2064         // to 8888 in case alpha is added. The third image defaults to F16, so
2065         // even with postProcess it will only be promoted to 8888.
2066         boolean postProcessCancels[] = new boolean[] { true, true, false };
2067         boolean trueFalse[] = new boolean[] { true, false };
2068         SourceCreator f = mCreators[0];
2069         for (int i = 0; i < resIds.length; ++i) {
2070             int resId = resIds[i];
2071             l.doPostProcess = false;
2072             l.preferRamOverQuality = false;
2073             Bitmap normal = null;
2074             try {
2075                 normal = ImageDecoder.decodeBitmap(f.apply(resId), l);
2076             } catch (IOException e) {
2077                 fail("Failed with exception " + e);
2078             }
2079             int normalByteCount = normal.getAllocationByteCount();
2080             for (boolean doPostProcess : trueFalse) {
2081                 l.doPostProcess = doPostProcess;
2082                 l.preferRamOverQuality = true;
2083                 Bitmap saveRamOverQuality = null;
2084                 try {
2085                     saveRamOverQuality = ImageDecoder.decodeBitmap(f.apply(resId), l);
2086                 } catch (IOException e) {
2087                     fail("Failed with exception " + e);
2088                 }
2089                 int saveByteCount = saveRamOverQuality.getAllocationByteCount();
2090                 if (doPostProcess && postProcessCancels[i]) {
2091                     // Promoted to normal.
2092                     assertEquals(normalByteCount, saveByteCount);
2093                 } else {
2094                     assertTrue(saveByteCount < normalByteCount);
2095                 }
2096             }
2097         }
2098     }
2099 
2100     @Test
2101     public void testRespectOrientation() {
2102         boolean isWebp = false;
2103         // These 8 images test the 8 EXIF orientations. If the orientation is
2104         // respected, they all have the same dimensions: 100 x 80.
2105         // They are also identical (after adjusting), so compare them.
2106         Bitmap reference = null;
2107         for (int resId : new int[] { R.drawable.orientation_1,
2108                                      R.drawable.orientation_2,
2109                                      R.drawable.orientation_3,
2110                                      R.drawable.orientation_4,
2111                                      R.drawable.orientation_5,
2112                                      R.drawable.orientation_6,
2113                                      R.drawable.orientation_7,
2114                                      R.drawable.orientation_8,
2115                                      R.drawable.webp_orientation1,
2116                                      R.drawable.webp_orientation2,
2117                                      R.drawable.webp_orientation3,
2118                                      R.drawable.webp_orientation4,
2119                                      R.drawable.webp_orientation5,
2120                                      R.drawable.webp_orientation6,
2121                                      R.drawable.webp_orientation7,
2122                                      R.drawable.webp_orientation8,
2123         }) {
2124             if (resId == R.drawable.webp_orientation1) {
2125                 // The webp files may not look exactly the same as the jpegs.
2126                 // Recreate the reference.
2127                 reference.recycle();
2128                 reference = null;
2129                 isWebp = true;
2130             }
2131             Uri uri = Utils.getAsResourceUri(resId);
2132             ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
2133             try {
2134                 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2135                     // Use software allocator so we can compare.
2136                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2137                 });
2138                 assertNotNull(bm);
2139                 assertEquals(100, bm.getWidth());
2140                 assertEquals(80,  bm.getHeight());
2141 
2142                 if (reference == null) {
2143                     reference = bm;
2144                 } else {
2145                     int mse = isWebp ? 70 : 1;
2146                     BitmapUtils.assertBitmapsMse(bm, reference, mse, true, false);
2147                     bm.recycle();
2148                 }
2149             } catch (IOException e) {
2150                 fail("Decoding " + uri.toString() + " yielded " + e);
2151             }
2152         }
2153     }
2154 
2155     @Test
testOrientationWithSampleSize()2156     public void testOrientationWithSampleSize() {
2157         Uri uri = Utils.getAsResourceUri(R.drawable.orientation_6);
2158         ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
2159         final int sampleSize = 7;
2160         try {
2161             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2162                 decoder.setTargetSampleSize(sampleSize);
2163             });
2164             assertNotNull(bm);
2165 
2166             // The unsampled image, after rotation, is 100 x 80
2167             assertEquals(100 / sampleSize, bm.getWidth());
2168             assertEquals( 80 / sampleSize, bm.getHeight());
2169         } catch (IOException e) {
2170             fail("Failed to decode " + uri.toString() + " with a sampleSize (" + sampleSize + ")");
2171         }
2172     }
2173 
2174     @Test(expected = ArrayIndexOutOfBoundsException.class)
testArrayOutOfBounds()2175     public void testArrayOutOfBounds() {
2176         byte[] array = new byte[10];
2177         ImageDecoder.createSource(array, 1, 10);
2178     }
2179 
2180     @Test(expected = ArrayIndexOutOfBoundsException.class)
testOffsetOutOfBounds()2181     public void testOffsetOutOfBounds() {
2182         byte[] array = new byte[10];
2183         ImageDecoder.createSource(array, 10, 0);
2184     }
2185 
2186     @Test(expected = ArrayIndexOutOfBoundsException.class)
testLengthOutOfBounds()2187     public void testLengthOutOfBounds() {
2188         byte[] array = new byte[10];
2189         ImageDecoder.createSource(array, 0, 11);
2190     }
2191 
2192     @Test(expected = ArrayIndexOutOfBoundsException.class)
testNegativeLength()2193     public void testNegativeLength() {
2194         byte[] array = new byte[10];
2195         ImageDecoder.createSource(array, 0, -1);
2196     }
2197 
2198     @Test(expected = ArrayIndexOutOfBoundsException.class)
testNegativeOffset()2199     public void testNegativeOffset() {
2200         byte[] array = new byte[10];
2201         ImageDecoder.createSource(array, -1, 10);
2202     }
2203 
2204     @Test(expected = NullPointerException.class)
testNullByteArray()2205     public void testNullByteArray() {
2206         ImageDecoder.createSource(null, 0, 0);
2207     }
2208 
2209     @Test(expected = NullPointerException.class)
testNullByteArray2()2210     public void testNullByteArray2() {
2211         byte[] array = null;
2212         ImageDecoder.createSource(array);
2213     }
2214 
2215     @Test(expected = IOException.class)
testZeroLengthByteArray()2216     public void testZeroLengthByteArray() throws IOException {
2217         ImageDecoder.decodeDrawable(ImageDecoder.createSource(new byte[10], 0, 0));
2218     }
2219 
2220     @Test(expected = IOException.class)
testZeroLengthByteBuffer()2221     public void testZeroLengthByteBuffer() throws IOException {
2222         ImageDecoder.decodeDrawable(ImageDecoder.createSource(ByteBuffer.wrap(new byte[10], 0, 0)));
2223     }
2224 
2225     private interface ByteBufferSupplier extends Supplier<ByteBuffer> {};
2226 
2227     @Test
2228     @Parameters(method = "getRecords")
testOffsetByteArray(Record record)2229     public void testOffsetByteArray(Record record) {
2230         int offset = 10;
2231         int extra = 15;
2232         byte[] array = getAsByteArray(record.resId, offset, extra);
2233         int length = array.length - extra - offset;
2234         // Used for SourceCreators that set both a position and an offset.
2235         int myOffset = 3;
2236         int myPosition = 7;
2237         assertEquals(offset, myOffset + myPosition);
2238 
2239         ByteBufferSupplier[] suppliers = new ByteBufferSupplier[] {
2240                 // Internally, this gives the buffer a position, but not an offset.
2241                 () -> ByteBuffer.wrap(array, offset, length),
2242                 // Same, but make it readOnly to ensure that we test the
2243                 // ByteBufferSource rather than the ByteArraySource.
2244                 () -> ByteBuffer.wrap(array, offset, length).asReadOnlyBuffer(),
2245                 () -> {
2246                     // slice() to give the buffer an offset.
2247                     ByteBuffer buf = ByteBuffer.wrap(array, 0, array.length - extra);
2248                     buf.position(offset);
2249                     return buf.slice();
2250                 },
2251                 () -> {
2252                     // Same, but make it readOnly to ensure that we test the
2253                     // ByteBufferSource rather than the ByteArraySource.
2254                     ByteBuffer buf = ByteBuffer.wrap(array, 0, array.length - extra);
2255                     buf.position(offset);
2256                     return buf.slice().asReadOnlyBuffer();
2257                 },
2258                 () -> {
2259                     // Use both a position and an offset.
2260                     ByteBuffer buf = ByteBuffer.wrap(array, myOffset,
2261                             array.length - extra - myOffset);
2262                     buf = buf.slice();
2263                     buf.position(myPosition);
2264                     return buf;
2265                 },
2266                 () -> {
2267                     // Same, as readOnly.
2268                     ByteBuffer buf = ByteBuffer.wrap(array, myOffset,
2269                             array.length - extra - myOffset);
2270                     buf = buf.slice();
2271                     buf.position(myPosition);
2272                     return buf.asReadOnlyBuffer();
2273                 },
2274                 () -> {
2275                     // Direct ByteBuffer with a position.
2276                     ByteBuffer buf = ByteBuffer.allocateDirect(array.length);
2277                     buf.put(array);
2278                     buf.position(offset);
2279                     return buf;
2280                 },
2281                 () -> {
2282                     // Sliced direct ByteBuffer, for an offset.
2283                     ByteBuffer buf = ByteBuffer.allocateDirect(array.length);
2284                     buf.put(array);
2285                     buf.position(offset);
2286                     return buf.slice();
2287                 },
2288                 () -> {
2289                     // Direct ByteBuffer with position and offset.
2290                     ByteBuffer buf = ByteBuffer.allocateDirect(array.length);
2291                     buf.put(array);
2292                     buf.position(myOffset);
2293                     buf = buf.slice();
2294                     buf.position(myPosition);
2295                     return buf;
2296                 },
2297         };
2298         for (int i = 0; i < suppliers.length; ++i) {
2299             ByteBuffer buffer = suppliers[i].get();
2300             final int position = buffer.position();
2301             ImageDecoder.Source src = ImageDecoder.createSource(buffer);
2302             try {
2303                 Drawable drawable = ImageDecoder.decodeDrawable(src);
2304                 assertNotNull(drawable);
2305             } catch (IOException e) {
2306                 fail("Failed with exception " + e);
2307             }
2308             assertEquals("Mismatch for supplier " + i,
2309                     position, buffer.position());
2310         }
2311     }
2312 
2313     @Test
2314     @Parameters(method = "getRecords")
testOffsetByteArray2(Record record)2315     public void testOffsetByteArray2(Record record) throws IOException {
2316         ImageDecoder.Source src = ImageDecoder.createSource(getAsByteArray(record.resId));
2317         Bitmap expected = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2318             decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2319         });
2320 
2321         final int offset = 10;
2322         final int extra = 15;
2323         final byte[] array = getAsByteArray(record.resId, offset, extra);
2324         src = ImageDecoder.createSource(array, offset, array.length - (offset + extra));
2325         Bitmap actual = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2326             decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2327         });
2328         assertTrue(actual.sameAs(expected));
2329     }
2330 
2331     @Test
2332     @Parameters(method = "getRecords")
testResourceSource(Record record)2333     public void testResourceSource(Record record) {
2334         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), record.resId);
2335         try {
2336             Drawable drawable = ImageDecoder.decodeDrawable(src);
2337             assertNotNull(drawable);
2338         } catch (IOException e) {
2339             fail("Failed " + Utils.getAsResourceUri(record.resId) + " with " + e);
2340         }
2341     }
2342 
decodeBitmapDrawable(int resId)2343     private BitmapDrawable decodeBitmapDrawable(int resId) {
2344         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), resId);
2345         try {
2346             Drawable drawable = ImageDecoder.decodeDrawable(src);
2347             assertNotNull(drawable);
2348             assertTrue(drawable instanceof BitmapDrawable);
2349             return (BitmapDrawable) drawable;
2350         } catch (IOException e) {
2351             fail("Failed " + Utils.getAsResourceUri(resId) + " with " + e);
2352             return null;
2353         }
2354     }
2355 
2356     @Test
2357     @Parameters(method = "getRecords")
testUpscale(Record record)2358     public void testUpscale(Record record) {
2359         Resources res = getResources();
2360         final int originalDensity = res.getDisplayMetrics().densityDpi;
2361 
2362         try {
2363             final int resId = record.resId;
2364 
2365             // Set a high density. This will result in a larger drawable, but
2366             // not a larger Bitmap.
2367             res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_XXXHIGH;
2368             BitmapDrawable drawable = decodeBitmapDrawable(resId);
2369 
2370             Bitmap bm = drawable.getBitmap();
2371             assertEquals(record.width, bm.getWidth());
2372             assertEquals(record.height, bm.getHeight());
2373 
2374             assertTrue(drawable.getIntrinsicWidth() > record.width);
2375             assertTrue(drawable.getIntrinsicHeight() > record.height);
2376 
2377             // Set a low density. This will result in a smaller drawable and
2378             // Bitmap, unless the true density is DENSITY_MEDIUM, which matches
2379             // the density of the asset.
2380             res.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_LOW;
2381             drawable = decodeBitmapDrawable(resId);
2382             bm = drawable.getBitmap();
2383 
2384             if (originalDensity == DisplayMetrics.DENSITY_MEDIUM) {
2385                 // Although we've modified |densityDpi|, ImageDecoder knows the
2386                 // true density matches the asset, so it will not downscale at
2387                 // decode time.
2388                 assertEquals(bm.getWidth(), record.width);
2389                 assertEquals(bm.getHeight(), record.height);
2390 
2391                 // The drawable should still be smaller.
2392                 assertTrue(bm.getWidth() > drawable.getIntrinsicWidth());
2393                 assertTrue(bm.getHeight() > drawable.getIntrinsicHeight());
2394             } else {
2395                 // The bitmap is scaled down at decode time, so it matches the
2396                 // drawable size, and is smaller than the original.
2397                 assertTrue(bm.getWidth() < record.width);
2398                 assertTrue(bm.getHeight() < record.height);
2399 
2400                 assertEquals(bm.getWidth(), drawable.getIntrinsicWidth());
2401                 assertEquals(bm.getHeight(), drawable.getIntrinsicHeight());
2402             }
2403         } finally {
2404             res.getDisplayMetrics().densityDpi = originalDensity;
2405         }
2406     }
2407 
2408     static class AssetRecord {
2409         public final String name;
2410         public final int width;
2411         public final int height;
2412         public final boolean isF16;
2413         public final boolean isGray;
2414         public final boolean hasAlpha;
2415         private final ColorSpace mColorSpace;
2416 
2417         AssetRecord(String name, int width, int height, boolean isF16,
2418                 boolean isGray, boolean hasAlpha, ColorSpace colorSpace) {
2419             this.name = name;
2420             this.width = width;
2421             this.height = height;
2422             this.isF16 = isF16;
2423             this.isGray = isGray;
2424             this.hasAlpha = hasAlpha;
2425             mColorSpace = colorSpace;
2426         }
2427 
2428         public ColorSpace getColorSpace() {
2429             return mColorSpace;
2430         }
2431 
2432         public void checkColorSpace(ColorSpace requested, ColorSpace actual) {
2433             assertNotNull("Null ColorSpace for " + this.name, actual);
2434             if (this.isF16 && requested != null) {
2435                 if (requested == ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)) {
2436                     assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), actual);
2437                 } else if (requested == ColorSpace.get(ColorSpace.Named.SRGB)) {
2438                     assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), actual);
2439                 } else {
2440                     assertSame(requested, actual);
2441                 }
2442             } else if (requested != null) {
2443                 // If the asset is *not* 16 bit, requesting EXTENDED will promote to 16 bit.
2444                 assertSame(requested, actual);
2445             } else if (mColorSpace == null) {
2446                 assertEquals(this.name, "Unknown", actual.getName());
2447             } else {
2448                 assertSame(this.name, mColorSpace, actual);
2449             }
2450         }
2451     }
2452 
2453     static Object[] getAssetRecords() {
2454         return new Object [] {
2455             // A null ColorSpace means that the color space is "Unknown".
2456             new AssetRecord("almost-red-adobe.png", 1, 1, false, false, false, null),
2457             new AssetRecord("green-p3.png", 64, 64, false, false, false,
2458                     ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
2459             new AssetRecord("green-srgb.png", 64, 64, false, false, false, sSRGB),
2460             new AssetRecord("blue-16bit-prophoto.png", 100, 100, true, false, true,
2461                     ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB)),
2462             new AssetRecord("blue-16bit-srgb.png", 64, 64, true, false, false,
2463                     ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)),
2464             new AssetRecord("purple-cmyk.png", 64, 64, false, false, false, sSRGB),
2465             new AssetRecord("purple-displayprofile.png", 64, 64, false, false, false, null),
2466             new AssetRecord("red-adobergb.png", 64, 64, false, false, false,
2467                     ColorSpace.get(ColorSpace.Named.ADOBE_RGB)),
2468             new AssetRecord("translucent-green-p3.png", 64, 64, false, false, true,
2469                     ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
2470             new AssetRecord("grayscale-linearSrgb.png", 32, 32, false, true, false,
2471                     ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)),
2472             new AssetRecord("grayscale-16bit-linearSrgb.png", 32, 32, true, false, true,
2473                     ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB)),
2474         };
2475     }
2476 
2477     @Test
2478     @Parameters(method = "getAssetRecords")
2479     public void testAssetSource(AssetRecord record) {
2480         AssetManager assets = getResources().getAssets();
2481         ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
2482         try {
2483             Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2484                 if (record.isF16) {
2485                     // CTS infrastructure fails to create F16 HARDWARE Bitmaps, so this
2486                     // switches to using software.
2487                     decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2488                 }
2489 
2490                 record.checkColorSpace(null, info.getColorSpace());
2491             });
2492             assertEquals(record.name, record.width, bm.getWidth());
2493             assertEquals(record.name, record.height, bm.getHeight());
2494             record.checkColorSpace(null, bm.getColorSpace());
2495             assertEquals(record.hasAlpha, bm.hasAlpha());
2496         } catch (IOException e) {
2497             fail("Failed to decode asset " + record.name + " with " + e);
2498         }
2499     }
2500 
2501     @Test
2502     @Parameters(method = "getAssetRecords")
2503     public void testTargetColorSpace(AssetRecord record) {
2504         AssetManager assets = getResources().getAssets();
2505         ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
2506         for (ColorSpace cs : BitmapTest.getRgbColorSpaces()) {
2507             try {
2508                 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2509                     if (record.isF16 || isExtended(cs)) {
2510                         // CTS infrastructure and some devices fail to create F16
2511                         // HARDWARE Bitmaps, so this switches to using software.
2512                         decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2513                     }
2514                     decoder.setTargetColorSpace(cs);
2515                 });
2516                 record.checkColorSpace(cs, bm.getColorSpace());
2517             } catch (IOException e) {
2518                 fail("Failed to decode asset " + record.name + " to " + cs + " with " + e);
2519             }
2520         }
2521     }
2522 
2523     @Test
2524     @Parameters(method = "getAssetRecords")
testTargetColorSpaceNoF16HARDWARE(AssetRecord record)2525     public void testTargetColorSpaceNoF16HARDWARE(AssetRecord record) {
2526         final ColorSpace EXTENDED_SRGB = ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB);
2527         final ColorSpace LINEAR_EXTENDED_SRGB = ColorSpace.get(
2528                 ColorSpace.Named.LINEAR_EXTENDED_SRGB);
2529         AssetManager assets = getResources().getAssets();
2530         ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
2531         for (ColorSpace cs : new ColorSpace[] { EXTENDED_SRGB, LINEAR_EXTENDED_SRGB }) {
2532             try {
2533                 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2534                     decoder.setTargetColorSpace(cs);
2535                 });
2536                 // If the ColorSpace does not match the request, it should be because
2537                 // F16 + HARDWARE is not supported. In that case, it should match the non-
2538                 // EXTENDED variant.
2539                 ColorSpace actual = bm.getColorSpace();
2540                 if (actual != cs) {
2541                     assertEquals(BitmapTest.ANDROID_BITMAP_FORMAT_RGBA_8888,
2542                                  BitmapTest.nGetFormat(bm));
2543                     if (cs == EXTENDED_SRGB) {
2544                         assertSame(ColorSpace.get(ColorSpace.Named.SRGB), actual);
2545                     } else {
2546                         assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), actual);
2547                     }
2548                 }
2549             } catch (IOException e) {
2550                 fail("Failed to decode asset " + record.name + " to " + cs + " with " + e);
2551             }
2552         }
2553     }
2554 
isExtended(ColorSpace colorSpace)2555     private boolean isExtended(ColorSpace colorSpace) {
2556         return colorSpace == ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)
2557             || colorSpace == ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
2558     }
2559 
2560     @Test
2561     @Parameters(method = "getAssetRecords")
testTargetColorSpaceUpconvert(AssetRecord record)2562     public void testTargetColorSpaceUpconvert(AssetRecord record) {
2563         // Verify that decoding an asset to EXTENDED upconverts to F16.
2564         AssetManager assets = getResources().getAssets();
2565         boolean[] trueFalse = new boolean[] { true, false };
2566         final ColorSpace linearExtended = ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
2567         final ColorSpace linearSrgb = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB);
2568 
2569         if (record.isF16) {
2570             // These assets decode to F16 by default.
2571             return;
2572         }
2573         ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
2574         for (ColorSpace cs : BitmapTest.getRgbColorSpaces()) {
2575             for (boolean alphaMask : trueFalse) {
2576                 try {
2577                     Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2578                         // Force software so we can check the Config.
2579                         decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2580                         decoder.setTargetColorSpace(cs);
2581                         // This has no effect on non-gray assets.
2582                         decoder.setDecodeAsAlphaMaskEnabled(alphaMask);
2583                     });
2584 
2585                     if (record.isGray && alphaMask) {
2586                         assertSame(Bitmap.Config.ALPHA_8, bm.getConfig());
2587                         assertNull(bm.getColorSpace());
2588                     } else {
2589                         assertSame(cs, bm.getColorSpace());
2590                         if (isExtended(cs)) {
2591                             assertSame(Bitmap.Config.RGBA_F16, bm.getConfig());
2592                         } else {
2593                             assertSame(Bitmap.Config.ARGB_8888, bm.getConfig());
2594                         }
2595                     }
2596                 } catch (IOException e) {
2597                     fail("Failed to decode asset " + record.name + " to " + cs + " with " + e);
2598                 }
2599 
2600                 // Using MEMORY_POLICY_LOW_RAM prevents upconverting.
2601                 try {
2602                     Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
2603                         // Force software so we can check the Config.
2604                         decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2605                         decoder.setTargetColorSpace(cs);
2606                         decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
2607                         // This has no effect on non-gray assets.
2608                         decoder.setDecodeAsAlphaMaskEnabled(alphaMask);
2609                     });
2610 
2611                     assertNotEquals(Bitmap.Config.RGBA_F16, bm.getConfig());
2612 
2613                     if (record.isGray && alphaMask) {
2614                         assertSame(Bitmap.Config.ALPHA_8, bm.getConfig());
2615                         assertNull(bm.getColorSpace());
2616                     } else {
2617                         ColorSpace actual = bm.getColorSpace();
2618                         if (isExtended(cs)) {
2619                             if (cs == ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)) {
2620                                 assertSame(ColorSpace.get(ColorSpace.Named.SRGB), actual);
2621                             } else if (cs == linearExtended) {
2622                                 assertSame(linearSrgb, actual);
2623                             } else {
2624                                 fail("Test error: did isExtended() change?");
2625                             }
2626                         } else {
2627                             assertSame(cs, actual);
2628                             if (bm.hasAlpha()) {
2629                                 assertSame(Bitmap.Config.ARGB_8888, bm.getConfig());
2630                             } else {
2631                                 assertSame(Bitmap.Config.RGB_565, bm.getConfig());
2632                             }
2633                         }
2634                     }
2635                 } catch (IOException e) {
2636                     fail("Failed to decode asset " + record.name
2637                             + " with MEMORY_POLICY_LOW_RAM to " + cs + " with " + e);
2638                 }
2639             }
2640         }
2641     }
2642 
2643     @Test
testTargetColorSpaceIllegal()2644     public void testTargetColorSpaceIllegal() {
2645         ColorSpace noTransferParamsCS = new ColorSpace.Rgb("NoTransferParams",
2646                 new float[]{ 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f },
2647                 ColorSpace.ILLUMINANT_D50,
2648                 x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f),
2649                 0, 1);
2650         for (int resId : new int[] { R.drawable.png_test, R.drawable.animated }) {
2651             ImageDecoder.Source src = mCreators[0].apply(resId);
2652             for (ColorSpace cs : new ColorSpace[] {
2653                     ColorSpace.get(ColorSpace.Named.CIE_LAB),
2654                     ColorSpace.get(ColorSpace.Named.CIE_XYZ),
2655                     noTransferParamsCS,
2656             }) {
2657                 try {
2658                     ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
2659                         decoder.setTargetColorSpace(cs);
2660                     });
2661                     fail("Should have thrown an IllegalArgumentException for setTargetColorSpace("
2662                             + cs + ")!");
2663                 } catch (IOException e) {
2664                     fail("Failed to decode png_test with " + e);
2665                 } catch (IllegalArgumentException illegal) {
2666                     // This is expected.
2667                 }
2668             }
2669         }
2670     }
2671 
drawToBitmap(Drawable dr)2672     private Bitmap drawToBitmap(Drawable dr) {
2673         Bitmap bm = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(),
2674                 Bitmap.Config.ARGB_8888);
2675         Canvas canvas = new Canvas(bm);
2676         dr.draw(canvas);
2677         return bm;
2678     }
2679 
testReuse(ImageDecoder.Source src, String name)2680     private void testReuse(ImageDecoder.Source src, String name) {
2681         Drawable first = null;
2682         try {
2683             first = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
2684                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2685             });
2686         } catch (IOException e) {
2687             fail("Failed on first decode of " + name + " using " + src + "!");
2688         }
2689 
2690         Drawable second = null;
2691         try {
2692             second = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
2693                 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
2694             });
2695         } catch (IOException e) {
2696             fail("Failed on second decode of " + name + " using " + src + "!");
2697         }
2698 
2699         assertEquals(first.getIntrinsicWidth(), second.getIntrinsicWidth());
2700         assertEquals(first.getIntrinsicHeight(), second.getIntrinsicHeight());
2701 
2702         Bitmap bm1 = drawToBitmap(first);
2703         Bitmap bm2 = drawToBitmap(second);
2704         assertTrue(BitmapUtils.compareBitmaps(bm1, bm2));
2705     }
2706 
2707     @Test
testJpegInfiniteLoop()2708     public void testJpegInfiniteLoop() {
2709         ImageDecoder.Source src = mCreators[0].apply(R.raw.b78329453);
2710         try {
2711             ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
2712                 decoder.setTargetSampleSize(19);
2713             });
2714         } catch (IOException e) {
2715             fail();
2716         }
2717     }
2718 
getRecordsAsSources()2719     private Object[] getRecordsAsSources() {
2720         return Utils.crossProduct(getRecords(), mCreators);
2721     }
2722 
2723     @Test
2724     @LargeTest
2725     @Parameters(method = "getRecordsAsSources")
testReuse(Record record, SourceCreator f)2726     public void testReuse(Record record, SourceCreator f) {
2727         if (record.mimeType.equals("image/heif") || record.mimeType.equals("image/avif")) {
2728             // These images take too long for this test.
2729             return;
2730         }
2731 
2732         String name = Utils.getAsResourceUri(record.resId).toString();
2733         ImageDecoder.Source src = f.apply(record.resId);
2734         testReuse(src, name);
2735     }
2736 
2737     @Test
2738     @Parameters(method = "getRecords")
testReuse2(Record record)2739     public void testReuse2(Record record) {
2740         if (record.mimeType.equals("image/heif") || record.mimeType.equals("image/avif")) {
2741             // These images take too long for this test.
2742             return;
2743         }
2744 
2745         String name = Utils.getAsResourceUri(record.resId).toString();
2746         ImageDecoder.Source src = ImageDecoder.createSource(getResources(), record.resId);
2747         testReuse(src, name);
2748 
2749         src = ImageDecoder.createSource(getAsFile(record.resId));
2750         testReuse(src, name);
2751     }
2752 
getRecordsAsUris()2753     private Object[] getRecordsAsUris() {
2754         return Utils.crossProduct(getRecords(), mUriCreators);
2755     }
2756 
2757 
2758     @Test
2759     @Parameters(method = "getRecordsAsUris")
testReuseUri(Record record, UriCreator f)2760     public void testReuseUri(Record record, UriCreator f) {
2761         if (record.mimeType.equals("image/heif") || record.mimeType.equals("image/avif")) {
2762             // These images take too long for this test.
2763             return;
2764         }
2765 
2766         Uri uri = f.apply(record.resId);
2767         ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
2768         testReuse(src, uri.toString());
2769     }
2770 
2771     @Test
2772     @Parameters(method = "getAssetRecords")
testReuseAssetRecords(AssetRecord record)2773     public void testReuseAssetRecords(AssetRecord record) {
2774         AssetManager assets = getResources().getAssets();
2775         ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
2776         testReuse(src, record.name);
2777     }
2778 
2779 
2780     @Test
testReuseAnimated()2781     public void testReuseAnimated() {
2782         ImageDecoder.Source src = mCreators[0].apply(R.drawable.animated);
2783         testReuse(src, "animated.gif");
2784     }
2785 
2786     @Test
testIsMimeTypeSupported()2787     public void testIsMimeTypeSupported() {
2788         for (Object r : getRecords()) {
2789             Record record = (Record) r;
2790             assertTrue(record.mimeType, ImageDecoder.isMimeTypeSupported(record.mimeType));
2791         }
2792 
2793         for (String mimeType : new String[] {
2794                 "image/vnd.wap.wbmp",
2795                 "image/x-sony-arw",
2796                 "image/x-canon-cr2",
2797                 "image/x-adobe-dng",
2798                 "image/x-nikon-nef",
2799                 "image/x-nikon-nrw",
2800                 "image/x-olympus-orf",
2801                 "image/x-fuji-raf",
2802                 "image/x-panasonic-rw2",
2803                 "image/x-pentax-pef",
2804                 "image/x-samsung-srw",
2805         }) {
2806             assertTrue(mimeType, ImageDecoder.isMimeTypeSupported(mimeType));
2807         }
2808 
2809         assertEquals("image/heic", ImageDecoder.isMimeTypeSupported("image/heic"),
2810                 MediaUtils.hasDecoder(MediaFormat.MIMETYPE_VIDEO_HEVC));
2811 
2812         assertFalse(ImageDecoder.isMimeTypeSupported("image/x-does-not-exist"));
2813     }
2814 
2815     @Test(expected = FileNotFoundException.class)
testBadUri()2816     public void testBadUri() throws IOException {
2817         Uri uri = new Uri.Builder()
2818                 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
2819                 .authority("authority")
2820                 .appendPath("drawable")
2821                 .appendPath("bad")
2822                 .build();
2823         ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
2824         ImageDecoder.decodeDrawable(src);
2825     }
2826 
2827     @Test(expected = FileNotFoundException.class)
testBadUri2()2828     public void testBadUri2() throws IOException {
2829         // This URI will attempt to open a file from EmptyProvider, which always
2830         // returns null. This test ensures that we throw FileNotFoundException,
2831         // instead of a NullPointerException when attempting to dereference null.
2832         Uri uri = Uri.parse(ContentResolver.SCHEME_CONTENT + "://"
2833                 + "android.graphics.cts.assets/bad");
2834         ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
2835         ImageDecoder.decodeDrawable(src);
2836     }
2837 
2838     @Test(expected = FileNotFoundException.class)
testUriWithoutScheme()2839     public void testUriWithoutScheme() throws IOException {
2840         Uri uri = new Uri.Builder()
2841                 .authority("authority")
2842                 .appendPath("missing")
2843                 .appendPath("scheme")
2844                 .build();
2845         ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
2846         ImageDecoder.decodeDrawable(src);
2847     }
2848 
2849     @Test(expected = FileNotFoundException.class)
testBadCallable()2850     public void testBadCallable() throws IOException {
2851         ImageDecoder.Source src = ImageDecoder.createSource(() -> null);
2852         ImageDecoder.decodeDrawable(src);
2853     }
2854 
has10BitHEVCDecoder()2855     private static boolean has10BitHEVCDecoder() {
2856         MediaFormat format = new MediaFormat();
2857         format.setString(MediaFormat.KEY_MIME, "video/hevc");
2858         format.setInteger(
2859             MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10);
2860         format.setInteger(
2861             MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel5);
2862 
2863         MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS);
2864         if (mcl.findDecoderForFormat(format) == null) {
2865             return false;
2866         }
2867         return true;
2868     }
2869 
hasHEVCDecoderSupportsYUVP010()2870     private static boolean hasHEVCDecoderSupportsYUVP010() {
2871         MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
2872         for (MediaCodecInfo mediaCodecInfo : codecList.getCodecInfos()) {
2873             if (mediaCodecInfo.isEncoder()) {
2874                 continue;
2875             }
2876             for (String mediaType : mediaCodecInfo.getSupportedTypes()) {
2877                 if (mediaType.equalsIgnoreCase("video/hevc")) {
2878                     MediaCodecInfo.CodecCapabilities codecCapabilities =
2879                             mediaCodecInfo.getCapabilitiesForType(mediaType);
2880                     for (int i = 0; i < codecCapabilities.colorFormats.length; ++i) {
2881                         if (codecCapabilities.colorFormats[i]
2882                                 == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010) {
2883                             return true;
2884                         }
2885                     }
2886                 }
2887             }
2888         }
2889         return false;
2890     }
2891 }
2892