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