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