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