Merge "Add support for post-decode density scaling with reuse"
diff --git a/core/jni/android/graphics/BitmapFactory.cpp b/core/jni/android/graphics/BitmapFactory.cpp
index 38dee1b..a7a9266 100644
--- a/core/jni/android/graphics/BitmapFactory.cpp
+++ b/core/jni/android/graphics/BitmapFactory.cpp
@@ -29,6 +29,10 @@
 jfieldID gOptions_purgeableFieldID;
 jfieldID gOptions_shareableFieldID;
 jfieldID gOptions_preferQualityOverSpeedFieldID;
+jfieldID gOptions_scaledFieldID;
+jfieldID gOptions_densityFieldID;
+jfieldID gOptions_screenDensityFieldID;
+jfieldID gOptions_targetDensityFieldID;
 jfieldID gOptions_widthFieldID;
 jfieldID gOptions_heightFieldID;
 jfieldID gOptions_mimeFieldID;
@@ -152,10 +156,47 @@
     return pr;
 }
 
+static SkBitmap::Config configForScaledOutput(SkBitmap::Config config) {
+    switch (config) {
+        case SkBitmap::kNo_Config:
+        case SkBitmap::kIndex8_Config:
+        case SkBitmap::kRLE_Index8_Config:
+            return SkBitmap::kARGB_8888_Config;
+        default:
+            break;
+    }
+    return config;
+}
+
+class ScaleCheckingAllocator : public SkBitmap::HeapAllocator {
+public:
+    ScaleCheckingAllocator(float scale, int size)
+            : mScale(scale), mSize(size) {
+    }
+
+    virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
+        // accounts for scale in final allocation, using eventual size and config
+        const int bytesPerPixel = SkBitmap::ComputeBytesPerPixel(
+                configForScaledOutput(bitmap->getConfig()));
+        const int requestedSize = bytesPerPixel *
+                int(bitmap->width() * mScale + 0.5f) *
+                int(bitmap->height() * mScale + 0.5f);
+        if (requestedSize > mSize) {
+            ALOGW("bitmap for alloc reuse (%d bytes) can't fit scaled bitmap (%d bytes)",
+                    mSize, requestedSize);
+            return false;
+        }
+        return SkBitmap::HeapAllocator::allocPixelRef(bitmap, ctable);
+    }
+private:
+    const float mScale;
+    const int mSize;
+};
+
 class RecyclingPixelAllocator : public SkBitmap::Allocator {
 public:
     RecyclingPixelAllocator(SkPixelRef* pixelRef, unsigned int size)
-        : mPixelRef(pixelRef), mSize(size) {
+            : mPixelRef(pixelRef), mSize(size) {
         SkSafeRef(mPixelRef);
     }
 
@@ -165,8 +206,8 @@
 
     virtual bool allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) {
         if (!bitmap->getSize64().is32() || bitmap->getSize() > mSize) {
-            ALOGW("bitmap marked for reuse (%d bytes) too small to contain new bitmap (%d bytes)",
-                    bitmap->getSize(), mSize);
+            ALOGW("bitmap marked for reuse (%d bytes) can't fit new bitmap (%d bytes)",
+                    mSize, bitmap->getSize());
             return false;
         }
         bitmap->setPixelRef(mPixelRef);
@@ -183,8 +224,7 @@
 // i.e. dynamically allocated, since its lifetime may exceed the current stack
 // frame.
 static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,
-        jobject options, bool allowPurgeable, bool forcePurgeable = false,
-        bool applyScale = false, float scale = 1.0f) {
+        jobject options, bool allowPurgeable, bool forcePurgeable = false) {
 
     int sampleSize = 1;
 
@@ -193,9 +233,8 @@
 
     bool doDither = true;
     bool isMutable = false;
-    bool willScale = applyScale && scale != 1.0f;
-    bool isPurgeable = !willScale &&
-            (forcePurgeable || (allowPurgeable && optionsPurgeable(env, options)));
+    float scale = 1.0f;
+    bool isPurgeable = forcePurgeable || (allowPurgeable && optionsPurgeable(env, options));
     bool preferQualityOverSpeed = false;
 
     jobject javaBitmap = NULL;
@@ -218,12 +257,19 @@
         preferQualityOverSpeed = env->GetBooleanField(options,
                 gOptions_preferQualityOverSpeedFieldID);
         javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
+
+        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
+            const int density = env->GetIntField(options, gOptions_densityFieldID);
+            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
+            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
+            if (density != 0 && targetDensity != 0 && density != screenDensity) {
+                scale = (float) targetDensity / density;
+            }
+        }
     }
 
-    // TODO: allow scaling with reuse, ideally avoiding decode in not-enough-space condition
-    if (willScale && javaBitmap != NULL) {
-        return nullObjectReturn("Cannot pre-scale a reused bitmap");
-    }
+    const bool willScale = scale != 1.0f;
+    isPurgeable &= !willScale;
 
     SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
     if (decoder == NULL) {
@@ -254,16 +300,6 @@
 
     NinePatchPeeker peeker(decoder);
     decoder->setPeeker(&peeker);
-    JavaPixelAllocator javaAllocator(env);
-    RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize);
-
-    // allocator to be used for final allocation associated with output
-    SkBitmap::Allocator* allocator = (javaBitmap != NULL) ?
-            (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
-
-    if (!isPurgeable && !willScale) {
-        decoder->setAllocator(allocator);
-    }
 
     AutoDecoderCancel adc(options, decoder);
 
@@ -276,6 +312,22 @@
 
     SkImageDecoder::Mode decodeMode = isPurgeable ? SkImageDecoder::kDecodeBounds_Mode : mode;
 
+
+    JavaPixelAllocator javaAllocator(env);
+    RecyclingPixelAllocator recyclingAllocator(outputBitmap->pixelRef(), existingBufferSize);
+    ScaleCheckingAllocator scaleCheckingAllocator(scale, existingBufferSize);
+    SkBitmap::Allocator* outputAllocator = (javaBitmap != NULL) ?
+            (SkBitmap::Allocator*)&recyclingAllocator : (SkBitmap::Allocator*)&javaAllocator;
+    if (decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
+        if (!willScale) {
+            decoder->setAllocator(outputAllocator);
+        } else if (javaBitmap != NULL) {
+            // check for eventual scaled bounds at allocation time, so we don't decode the bitmap
+            // only to find the scaled result too large to fit in the allocation
+            decoder->setAllocator(&scaleCheckingAllocator);
+        }
+    }
+
     SkBitmap decodingBitmap;
     if (!decoder->decode(stream, &decodingBitmap, prefConfig, decodeMode)) {
         return nullObjectReturn("decoder->decode returned false");
@@ -353,20 +405,11 @@
         const float sx = scaledWidth / float(decodingBitmap.width());
         const float sy = scaledHeight / float(decodingBitmap.height());
 
-        SkBitmap::Config config = decodingBitmap.config();
-        switch (config) {
-            case SkBitmap::kNo_Config:
-            case SkBitmap::kIndex8_Config:
-            case SkBitmap::kRLE_Index8_Config:
-                config = SkBitmap::kARGB_8888_Config;
-                break;
-            default:
-                break;
-        }
-
+        // TODO: avoid copying when scaled size equals decodingBitmap size
+        SkBitmap::Config config = configForScaledOutput(decodingBitmap.config());
         outputBitmap->setConfig(config, scaledWidth, scaledHeight);
         outputBitmap->setIsOpaque(decodingBitmap.isOpaque());
-        if (!outputBitmap->allocPixels(allocator, NULL)) {
+        if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
             return nullObjectReturn("allocation failed for scaled bitmap");
         }
         outputBitmap->eraseColor(0);
@@ -422,26 +465,20 @@
             isMutable, ninePatchChunk, layoutBounds, -1);
 }
 
-static jobject nativeDecodeStreamScaled(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
-        jobject padding, jobject options, jboolean applyScale, jfloat scale) {
+static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
+        jobject padding, jobject options) {
 
     jobject bitmap = NULL;
     SkStream* stream = CreateJavaInputStreamAdaptor(env, is, storage, 0);
 
     if (stream) {
         // for now we don't allow purgeable with java inputstreams
-        bitmap = doDecode(env, stream, padding, options, false, false, applyScale, scale);
+        bitmap = doDecode(env, stream, padding, options, false, false);
         stream->unref();
     }
     return bitmap;
 }
 
-static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
-        jobject padding, jobject options) {
-
-    return nativeDecodeStreamScaled(env, clazz, is, storage, padding, options, false, 1.0f);
-}
-
 static ssize_t getFDSize(int fd) {
     off64_t curr = ::lseek64(fd, 0, SEEK_CUR);
     if (curr < 0) {
@@ -516,8 +553,8 @@
     return stream;
 }
 
-static jobject nativeDecodeAssetScaled(JNIEnv* env, jobject clazz, jint native_asset,
-        jobject padding, jobject options, jboolean applyScale, jfloat scale) {
+static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jint native_asset,
+        jobject padding, jobject options) {
 
     SkStream* stream;
     Asset* asset = reinterpret_cast<Asset*>(native_asset);
@@ -535,13 +572,7 @@
         stream = new AssetStreamAdaptor(asset);
     }
     SkAutoUnref aur(stream);
-    return doDecode(env, stream, padding, options, true, forcePurgeable, applyScale, scale);
-}
-
-static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz, jint native_asset,
-        jobject padding, jobject options) {
-
-    return nativeDecodeAssetScaled(env, clazz, native_asset, padding, options, false, 1.0f);
+    return doDecode(env, stream, padding, options, true, forcePurgeable);
 }
 
 static jobject nativeDecodeByteArray(JNIEnv* env, jobject, jbyteArray byteArray,
@@ -575,10 +606,6 @@
         "(Ljava/io/InputStream;[BLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
         (void*)nativeDecodeStream
     },
-    {   "nativeDecodeStream",
-        "(Ljava/io/InputStream;[BLandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;ZF)Landroid/graphics/Bitmap;",
-        (void*)nativeDecodeStreamScaled
-    },
 
     {   "nativeDecodeFileDescriptor",
         "(Ljava/io/FileDescriptor;Landroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
@@ -590,11 +617,6 @@
         (void*)nativeDecodeAsset
     },
 
-    {   "nativeDecodeAsset",
-        "(ILandroid/graphics/Rect;Landroid/graphics/BitmapFactory$Options;ZF)Landroid/graphics/Bitmap;",
-        (void*)nativeDecodeAssetScaled
-    },
-
     {   "nativeDecodeByteArray",
         "([BIILandroid/graphics/BitmapFactory$Options;)Landroid/graphics/Bitmap;",
         (void*)nativeDecodeByteArray
@@ -637,6 +659,10 @@
     gOptions_shareableFieldID = getFieldIDCheck(env, options_class, "inInputShareable", "Z");
     gOptions_preferQualityOverSpeedFieldID = getFieldIDCheck(env, options_class,
             "inPreferQualityOverSpeed", "Z");
+    gOptions_scaledFieldID = getFieldIDCheck(env, options_class, "inScaled", "Z");
+    gOptions_densityFieldID = getFieldIDCheck(env, options_class, "inDensity", "I");
+    gOptions_screenDensityFieldID = getFieldIDCheck(env, options_class, "inScreenDensity", "I");
+    gOptions_targetDensityFieldID = getFieldIDCheck(env, options_class, "inTargetDensity", "I");
     gOptions_widthFieldID = getFieldIDCheck(env, options_class, "outWidth", "I");
     gOptions_heightFieldID = getFieldIDCheck(env, options_class, "outHeight", "I");
     gOptions_mimeFieldID = getFieldIDCheck(env, options_class, "outMimeType", "Ljava/lang/String;");
diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java
index b89d042..deccac1 100644
--- a/graphics/java/android/graphics/BitmapFactory.java
+++ b/graphics/java/android/graphics/BitmapFactory.java
@@ -50,29 +50,30 @@
          * reuse this bitmap when loading content. If the decode operation cannot
          * use this bitmap, the decode method will return <code>null</code> and
          * will throw an IllegalArgumentException. The current implementation
-         * necessitates that the reused bitmap be mutable and of a size that is
-         * equal to or larger than the source content. The source content must be
-         * in jpeg or png format (whether as a resource or as a stream). The
-         * {@link android.graphics.Bitmap.Config configuration} of the reused
-         * bitmap will override the setting of {@link #inPreferredConfig}, if set.
-         * The reused bitmap will continue to remain mutable even when decoding a
+         * necessitates that the reused bitmap be mutable, and the resulting
+         * reused bitmap will continue to remain mutable even when decoding a
          * resource which would normally result in an immutable bitmap.
          *
-         * <p>As of {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, the reused
-         * bitmap can be used to decode any other bitmaps as long as the resulting
+         * <p>As of {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, any mutable
+         * bitmap can be reused to decode any other bitmaps as long as the resulting
          * {@link Bitmap#getByteCount() byte count} of the decoded bitmap is less
          * than or equal to the {@link Bitmap#getAllocationByteCount() allocated byte count}
          * of the reused bitmap. This can be because the intrinsic size is smaller,
-         * or the sampled size is smaller. Prior to
-         * {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE}, only equal sized
-         * bitmaps are supported, with {@link #inSampleSize} set to 1.
+         * or the size after density / sampled size scaling is smaller.
+         *
+         * <p>Prior to {@link android.os.Build.VERSION_CODES#KEY_LIME_PIE} additional
+         * constraints apply: The image being decoded (whether as a resource or
+         * as a stream) must be in jpeg or png format. Only equal sized bitmaps
+         * are supported, with {@link #inSampleSize} set to 1. Additionally, the
+         * {@link android.graphics.Bitmap.Config configuration} of the reused
+         * bitmap will override the setting of {@link #inPreferredConfig}, if set.
          *
          * <p>You should still always use the returned Bitmap of the decode
          * method and not assume that reusing the bitmap worked, due to the
          * constraints outlined above and failure situations that can occur.
          * Checking whether the return value matches the value of the inBitmap
-         * set in the Options structure is a way to see if the bitmap was reused,
-         * but in all cases you should use the returned Bitmap to make sure
+         * set in the Options structure will indicate if the bitmap was reused,
+         * but in all cases you should use the Bitmap returned by the decoding function to ensure
          * that you are using the bitmap that was used as the decode destination.</p>
          */
         public Bitmap inBitmap;
@@ -440,6 +441,7 @@
         if (bm == null && opts != null && opts.inBitmap != null) {
             throw new IllegalArgumentException("Problem decoding into existing bitmap");
         }
+        setDensityFromOptions(bm, opts);
         return bm;
     }
 
@@ -457,6 +459,31 @@
     }
 
     /**
+     * Set the newly decoded bitmap's density based on the Options.
+     */
+    private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
+        if (outputBitmap == null || opts == null) return;
+
+        final int density = opts.inDensity;
+        if (density != 0) {
+            outputBitmap.setDensity(density);
+            final int targetDensity = opts.inTargetDensity;
+            if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
+                return;
+            }
+
+            byte[] np = outputBitmap.getNinePatchChunk();
+            final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
+            if (opts.inScaled || isNinePatch) {
+                outputBitmap.setDensity(targetDensity);
+            }
+        } else if (opts.inBitmap != null) {
+            // bitmap was reused, ensure density is reset
+            outputBitmap.setDensity(Bitmap.getDefaultDensity());
+        }
+    }
+
+    /**
      * Decode an input stream into a bitmap. If the input stream is null, or
      * cannot be used to decode a bitmap, the function returns null.
      * The stream's position will be where ever it was after the encoded data
@@ -497,25 +524,7 @@
 
         if (is instanceof AssetManager.AssetInputStream) {
             final int asset = ((AssetManager.AssetInputStream) is).getAssetInt();
-
-            if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
-                float scale = 1.0f;
-                int targetDensity = 0;
-                if (opts != null) {
-                    final int density = opts.inDensity;
-                    targetDensity = opts.inTargetDensity;
-                    if (density != 0 && targetDensity != 0) {
-                        scale = targetDensity / (float) density;
-                    }
-                }
-
-                bm = nativeDecodeAsset(asset, outPadding, opts, true, scale);
-                if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
-
-                finish = false;
-            } else {
-                bm = nativeDecodeAsset(asset, outPadding, opts);
-            }
+            bm = nativeDecodeAsset(asset, outPadding, opts);
         } else {
             // pass some temp storage down to the native code. 1024 is made up,
             // but should be large enough to avoid too many small calls back
@@ -523,82 +532,18 @@
             // to mark(...) above.
             byte [] tempStorage = null;
             if (opts != null) tempStorage = opts.inTempStorage;
-            if (tempStorage == null) tempStorage = new byte[16 * 1024];
-
-            if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
-                float scale = 1.0f;
-                int targetDensity = 0;
-                if (opts != null) {
-                    final int density = opts.inDensity;
-                    targetDensity = opts.inTargetDensity;
-                    if (density != 0 && targetDensity != 0) {
-                        scale = targetDensity / (float) density;
-                    }
-                }
-
-                bm = nativeDecodeStream(is, tempStorage, outPadding, opts, true, scale);
-                if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
-
-                finish = false;
-            } else {
-                bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
-            }
+            if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
+            bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
         }
 
         if (bm == null && opts != null && opts.inBitmap != null) {
             throw new IllegalArgumentException("Problem decoding into existing bitmap");
         }
 
-        return finish ? finishDecode(bm, outPadding, opts) : bm;
-    }
-
-    // TODO: remove this path, implement any needed functionality in native decode
-    private static Bitmap finishDecode(Bitmap bm, Rect outPadding, Options opts) {
-        if (bm == null || opts == null) {
-            return bm;
-        }
-        
-        final int density = opts.inDensity;
-        if (density == 0) {
-            return bm;
-        }
-        
-        bm.setDensity(density);
-        final int targetDensity = opts.inTargetDensity;
-        if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
-            return bm;
-        }
-        byte[] np = bm.getNinePatchChunk();
-        int[] lb = bm.getLayoutBounds();
-        final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
-        if (opts.inScaled || isNinePatch) {
-            float scale = targetDensity / (float) density;
-            if (scale != 1.0f) {
-                final Bitmap oldBitmap = bm;
-                bm = Bitmap.createScaledBitmap(oldBitmap,
-                        Math.max(1, (int) (bm.getWidth() * scale + 0.5f)),
-                        Math.max(1, (int) (bm.getHeight() * scale + 0.5f)), true);
-                if (bm != oldBitmap) oldBitmap.recycle();
-
-                if (isNinePatch) {
-                    np = nativeScaleNinePatch(np, scale, outPadding);
-                    bm.setNinePatchChunk(np);
-                }
-                if (lb != null) {
-                    int[] newLb = new int[lb.length];
-                    for (int i=0; i<lb.length; i++) {
-                        newLb[i] = (int)((lb[i]*scale)+.5f);
-                    }
-                    bm.setLayoutBounds(newLb);
-                }
-            }
-
-            bm.setDensity(targetDensity);
-        }
-
+        setDensityFromOptions(bm, opts);
         return bm;
     }
-    
+
     /**
      * Decode an input stream into a bitmap. If the input stream is null, or
      * cannot be used to decode a bitmap, the function returns null.
@@ -633,7 +578,7 @@
             if (bm == null && opts != null && opts.inBitmap != null) {
                 throw new IllegalArgumentException("Problem decoding into existing bitmap");
             }
-            return finishDecode(bm, outPadding, opts);
+            return bm;
         } else {
             FileInputStream fis = new FileInputStream(fd);
             try {
@@ -660,8 +605,6 @@
 
     private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
             Rect padding, Options opts);
-    private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
-            Rect padding, Options opts, boolean applyScale, float scale);
     private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
             Rect padding, Options opts);
     private static native Bitmap nativeDecodeAsset(int asset, Rect padding, Options opts);