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