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