/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.messaging.util;

import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.MediaStore;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.view.View;

import com.android.messaging.Factory;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.media.ImageRequest;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.exif.ExifInterface;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.Files;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Arrays;

public class ImageUtils {
    private static final String TAG = LogUtil.BUGLE_TAG;
    private static final int MAX_OOM_COUNT = 1;
    private static final byte[] GIF87_HEADER = "GIF87a".getBytes(Charset.forName("US-ASCII"));
    private static final byte[] GIF89_HEADER = "GIF89a".getBytes(Charset.forName("US-ASCII"));

    // Used for drawBitmapWithCircleOnCanvas.
    // Default color is transparent for both circle background and stroke.
    public static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = 0;
    public static final int DEFAULT_CIRCLE_STROKE_COLOR = 0;

    private static volatile ImageUtils sInstance;

    public static ImageUtils get() {
        if (sInstance == null) {
            synchronized (ImageUtils.class) {
                if (sInstance == null) {
                    sInstance = new ImageUtils();
                }
            }
        }
        return sInstance;
    }

    @VisibleForTesting
    public static void set(final ImageUtils imageUtils) {
        sInstance = imageUtils;
    }

    /**
     * Transforms a bitmap into a byte array.
     *
     * @param quality Value between 0 and 100 that the compressor uses to discern what quality the
     *                resulting bytes should be
     * @param bitmap Bitmap to convert into bytes
     * @return byte array of bitmap
     */
    public static byte[] bitmapToBytes(final Bitmap bitmap, final int quality)
            throws OutOfMemoryError {
        boolean done = false;
        int oomCount = 0;
        byte[] imageBytes = null;
        while (!done) {
            try {
                final ByteArrayOutputStream os = new ByteArrayOutputStream();
                bitmap.compress(Bitmap.CompressFormat.JPEG, quality, os);
                imageBytes = os.toByteArray();
                done = true;
            } catch (final OutOfMemoryError e) {
                LogUtil.w(TAG, "OutOfMemory converting bitmap to bytes.");
                oomCount++;
                if (oomCount <= MAX_OOM_COUNT) {
                    Factory.get().reclaimMemory();
                } else {
                    done = true;
                    LogUtil.w(TAG, "Failed to convert bitmap to bytes. Out of Memory.");
                }
                throw e;
            }
        }
        return imageBytes;
    }

    /**
     * Given the source bitmap and a canvas, draws the bitmap through a circular
     * mask. Only draws a circle with diameter equal to the destination width.
     *
     * @param bitmap The source bitmap to draw.
     * @param canvas The canvas to draw it on.
     * @param source The source bound of the bitmap.
     * @param dest The destination bound on the canvas.
     * @param bitmapPaint Optional Paint object for the bitmap
     * @param fillBackground when set, fill the circle with backgroundColor
     * @param strokeColor draw a border outside the circle with strokeColor
     */
    public static void drawBitmapWithCircleOnCanvas(final Bitmap bitmap, final Canvas canvas,
            final RectF source, final RectF dest, @Nullable Paint bitmapPaint,
            final boolean fillBackground, final int backgroundColor, int strokeColor) {
        // Draw bitmap through shader first.
        final BitmapShader shader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
        final Matrix matrix = new Matrix();

        // Fit bitmap to bounds.
        matrix.setRectToRect(source, dest, Matrix.ScaleToFit.CENTER);

        shader.setLocalMatrix(matrix);

        if (bitmapPaint == null) {
            bitmapPaint = new Paint();
        }

        bitmapPaint.setAntiAlias(true);
        if (fillBackground) {
            bitmapPaint.setColor(backgroundColor);
            canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint);
        }

        bitmapPaint.setShader(shader);
        canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint);
        bitmapPaint.setShader(null);

        if (strokeColor != 0) {
            final Paint stroke = new Paint();
            stroke.setAntiAlias(true);
            stroke.setColor(strokeColor);
            stroke.setStyle(Paint.Style.STROKE);
            final float strokeWidth = 6f;
            stroke.setStrokeWidth(strokeWidth);
            canvas.drawCircle(dest.centerX(),
                    dest.centerX(),
                    dest.width() / 2f - stroke.getStrokeWidth() / 2f,
                    stroke);
        }
    }

    /**
     * Sets a drawable to the background of a view. setBackgroundDrawable() is deprecated since
     * JB and replaced by setBackground().
     */
    @SuppressWarnings("deprecation")
    public static void setBackgroundDrawableOnView(final View view, final Drawable drawable) {
        if (OsUtil.isAtLeastJB()) {
            view.setBackground(drawable);
        } else {
            view.setBackgroundDrawable(drawable);
        }
    }

    /**
     * Based on the input bitmap bounds given by BitmapFactory.Options, compute the required
     * sub-sampling size for loading a scaled down version of the bitmap to the required size
     * @param options a BitmapFactory.Options instance containing the bounds info of the bitmap
     * @param reqWidth the desired width of the bitmap. Can be ImageRequest.UNSPECIFIED_SIZE.
     * @param reqHeight the desired height of the bitmap.  Can be ImageRequest.UNSPECIFIED_SIZE.
     * @return
     */
    public int calculateInSampleSize(
            final BitmapFactory.Options options, final int reqWidth, final int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        final boolean checkHeight = reqHeight != ImageRequest.UNSPECIFIED_SIZE;
        final boolean checkWidth = reqWidth != ImageRequest.UNSPECIFIED_SIZE;
        if ((checkHeight && height > reqHeight) ||
                (checkWidth && width > reqWidth)) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((!checkHeight || (halfHeight / inSampleSize) > reqHeight)
                    && (!checkWidth || (halfWidth / inSampleSize) > reqWidth)) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }

    private static final String[] MEDIA_CONTENT_PROJECTION = new String[] {
        MediaStore.MediaColumns.MIME_TYPE
    };

    private static final int INDEX_CONTENT_TYPE = 0;

    @DoesNotRunOnMainThread
    public static String getContentType(final ContentResolver cr, final Uri uri) {
        // Figure out the content type of media.
        String contentType = null;
        Cursor cursor = null;
        if (UriUtil.isMediaStoreUri(uri)) {
            try {
                cursor = cr.query(uri, MEDIA_CONTENT_PROJECTION, null, null, null);

                if (cursor != null && cursor.moveToFirst()) {
                    contentType = cursor.getString(INDEX_CONTENT_TYPE);
                }
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }
        if (contentType == null) {
            // Last ditch effort to get the content type. Look at the file extension.
            contentType = ContentType.getContentTypeFromExtension(uri.toString(),
                    ContentType.IMAGE_UNSPECIFIED);
        }
        return contentType;
    }

    /**
     * @param context Android context
     * @param uri Uri to the image data
     * @return The exif orientation value for the image in the specified uri
     */
    public static int getOrientation(final Context context, final Uri uri) {
        try {
            return getOrientation(context.getContentResolver().openInputStream(uri));
        } catch (FileNotFoundException e) {
            LogUtil.e(TAG, "getOrientation couldn't open: " + uri, e);
        }
        return android.media.ExifInterface.ORIENTATION_UNDEFINED;
    }

    /**
     * @param inputStream The stream to the image file.  Closed on completion
     * @return The exif orientation value for the image in the specified stream
     */
    public static int getOrientation(final InputStream inputStream) {
        int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
        if (inputStream != null) {
            try {
                final ExifInterface exifInterface = new ExifInterface();
                exifInterface.readExif(inputStream);
                final Integer orientationValue =
                        exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
                if (orientationValue != null) {
                    orientation = orientationValue.intValue();
                }
            } catch (IOException e) {
                // If the image if GIF, PNG, or missing exif header, just use the defaults
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                } catch (IOException e) {
                    LogUtil.e(TAG, "getOrientation error closing input stream", e);
                }
            }
        }
        return orientation;
    }

    /**
     * Returns whether the resource is a GIF image.
     */
    public static boolean isGif(String contentType, Uri contentUri) {
        if (TextUtils.equals(contentType, ContentType.IMAGE_GIF)) {
            return true;
        }
        if (ContentType.isImageType(contentType)) {
            try {
                ContentResolver contentResolver = Factory.get().getApplicationContext()
                        .getContentResolver();
                InputStream inputStream = contentResolver.openInputStream(contentUri);
                return ImageUtils.isGif(inputStream);
            } catch (Exception e) {
                LogUtil.w(TAG, "Could not open GIF input stream", e);
            }
        }
        // Assume anything with a non-image content type is not a GIF
        return false;
    }

    /**
     * @param inputStream The stream to the image file. Closed on completion
     * @return Whether the image stream represents a GIF
     */
    public static boolean isGif(InputStream inputStream) {
        if (inputStream != null) {
            try {
                byte[] gifHeaderBytes = new byte[6];
                int value = inputStream.read(gifHeaderBytes, 0, 6);
                if (value == 6) {
                    return Arrays.equals(gifHeaderBytes, GIF87_HEADER)
                            || Arrays.equals(gifHeaderBytes, GIF89_HEADER);
                }
            } catch (IOException e) {
                return false;
            } finally {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
        return false;
    }

    /**
     * Read an image and compress it to particular max dimensions and size.
     * Used to ensure images can fit in an MMS.
     * TODO: This uses memory very inefficiently as it processes the whole image as a unit
     *  (rather than slice by slice) but system JPEG functions do not support slicing and dicing.
     */
    public static class ImageResizer {

        /**
         * The quality parameter which is used to compress JPEG images.
         */
        private static final int IMAGE_COMPRESSION_QUALITY = 95;
        /**
         * The minimum quality parameter which is used to compress JPEG images.
         */
        private static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50;

        /**
         * Minimum factor to reduce quality value
         */
        private static final double QUALITY_SCALE_DOWN_RATIO = 0.85f;

        /**
         * Maximum passes through the resize loop before failing permanently
         */
        private static final int NUMBER_OF_RESIZE_ATTEMPTS = 6;

        /**
         * Amount to scale down the picture when it doesn't fit
         */
        private static final float MIN_SCALE_DOWN_RATIO = 0.75f;

        /**
         * When computing sampleSize target scaling of no more than this ratio
         */
        private static final float MAX_TARGET_SCALE_FACTOR = 1.5f;


        // Current sample size for subsampling image during initial decode
        private int mSampleSize;
        // Current bitmap holding initial decoded source image
        private Bitmap mDecoded;
        // If scaling is needed this holds the scaled bitmap (else should equal mDecoded)
        private Bitmap mScaled;
        // Current JPEG compression quality to use when compressing image
        private int mQuality;
        // Current factor to scale down decoded image before compressing
        private float mScaleFactor;
        // Flag keeping track of whether cache memory has been reclaimed
        private boolean mHasReclaimedMemory;

        // Initial size of the image (typically provided but can be UNSPECIFIED_SIZE)
        private int mWidth;
        private int mHeight;
        // Orientation params of image as read from EXIF data
        private final ExifInterface.OrientationParams mOrientationParams;
        // Matrix to undo orientation and scale at the same time
        private final Matrix mMatrix;
        // Size limit as provided by MMS library
        private final int mWidthLimit;
        private final int mHeightLimit;
        private final int mByteLimit;
        //  Uri from which to read source image
        private final Uri mUri;
        // Application context
        private final Context mContext;
        // Cached value of bitmap factory options
        private final BitmapFactory.Options mOptions;
        private final String mContentType;

        private final int mMemoryClass;

        /**
         * Return resized (compressed) image (else null)
         *
         * @param width The width of the image (if known)
         * @param height The height of the image (if known)
         * @param orientation The orientation of the image as an ExifInterface constant
         * @param widthLimit The width limit, in pixels
         * @param heightLimit The height limit, in pixels
         * @param byteLimit The binary size limit, in bytes
         * @param uri Uri to the image data
         * @param context Needed to open the image
         * @param contentType of image
         * @return encoded image meeting size requirements else null
         */
        public static byte[] getResizedImageData(final int width, final int height,
                final int orientation, final int widthLimit, final int heightLimit,
                final int byteLimit, final Uri uri, final Context context,
                final String contentType) {
            final ImageResizer resizer = new ImageResizer(width, height, orientation,
                    widthLimit, heightLimit, byteLimit, uri, context, contentType);
            return resizer.resize();
        }

        /**
         * Create and initialize an image resizer
         */
        private ImageResizer(final int width, final int height, final int orientation,
                final int widthLimit, final int heightLimit, final int byteLimit, final Uri uri,
                final Context context, final String contentType) {
            mWidth = width;
            mHeight = height;
            mOrientationParams = ExifInterface.getOrientationParams(orientation);
            mMatrix = new Matrix();
            mWidthLimit = widthLimit;
            mHeightLimit = heightLimit;
            mByteLimit = byteLimit;
            mUri = uri;
            mWidth = width;
            mContext = context;
            mQuality = IMAGE_COMPRESSION_QUALITY;
            mScaleFactor = 1.0f;
            mHasReclaimedMemory = false;
            mOptions = new BitmapFactory.Options();
            mOptions.inScaled = false;
            mOptions.inDensity = 0;
            mOptions.inTargetDensity = 0;
            mOptions.inSampleSize = 1;
            mOptions.inJustDecodeBounds = false;
            mOptions.inMutable = false;
            final ActivityManager am =
                    (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
            mMemoryClass = Math.max(16, am.getMemoryClass());
            mContentType = contentType;
        }

        /**
         * Try to compress the image
         *
         * @return encoded image meeting size requirements else null
         */
        private byte[] resize() {
            return ImageUtils.isGif(mContentType, mUri) ? resizeGifImage() : resizeStaticImage();
        }

        private byte[] resizeGifImage() {
            byte[] bytesToReturn = null;
            final String inputFilePath;
            if (MediaScratchFileProvider.isMediaScratchSpaceUri(mUri)) {
                inputFilePath = MediaScratchFileProvider.getFileFromUri(mUri).getAbsolutePath();
            } else {
                if (!UriUtil.isFileUri(mUri)) {
                    Assert.fail("Expected a GIF file uri, but actual uri = " + mUri.toString());
                }
                inputFilePath = mUri.getPath();
            }

            if (GifTranscoder.canBeTranscoded(mWidth, mHeight)) {
                // Needed to perform the transcoding so that the gif can continue to play in the
                // conversation while the sending is taking place
                final Uri tmpUri = MediaScratchFileProvider.buildMediaScratchSpaceUri("gif");
                final File outputFile = MediaScratchFileProvider.getFileFromUri(tmpUri);
                final String outputFilePath = outputFile.getAbsolutePath();

                final boolean success =
                        GifTranscoder.transcode(mContext, inputFilePath, outputFilePath);
                if (success) {
                    try {
                        bytesToReturn = Files.toByteArray(outputFile);
                    } catch (IOException e) {
                        LogUtil.e(TAG, "Could not create FileInputStream with path of "
                                + outputFilePath, e);
                    }
                }

                // Need to clean up the new file created to compress the gif
                mContext.getContentResolver().delete(tmpUri, null, null);
            } else {
                // We don't want to transcode the gif because its image dimensions would be too
                // small so just return the bytes of the original gif
                try {
                    bytesToReturn = Files.toByteArray(new File(inputFilePath));
                } catch (IOException e) {
                    LogUtil.e(TAG,
                            "Could not create FileInputStream with path of " + inputFilePath, e);
                }
            }

            return bytesToReturn;
        }

        private byte[] resizeStaticImage() {
            if (!ensureImageSizeSet()) {
                // Cannot read image size
                return null;
            }
            // Find incoming image size
            if (!canBeCompressed()) {
                return null;
            }

            //  Decode image - if out of memory - reclaim memory and retry
            try {
                for (int attempts = 0; attempts < NUMBER_OF_RESIZE_ATTEMPTS; attempts++) {
                    final byte[] encoded = recodeImage(attempts);

                    // Only return data within the limit
                    if (encoded != null && encoded.length <= mByteLimit) {
                        return encoded;
                    } else {
                        final int currentSize = (encoded == null ? 0 : encoded.length);
                        updateRecodeParameters(currentSize);
                    }
                }
            } catch (final FileNotFoundException e) {
                LogUtil.e(TAG, "File disappeared during resizing");
            } finally {
                // Release all bitmaps
                if (mScaled != null && mScaled != mDecoded) {
                    mScaled.recycle();
                }
                if (mDecoded != null) {
                    mDecoded.recycle();
                }
            }
            return null;
        }

        /**
         * Ensure that the width and height of the source image are known
         * @return flag indicating whether size is known
         */
        private boolean ensureImageSizeSet() {
            if (mWidth == MessagingContentProvider.UNSPECIFIED_SIZE ||
                    mHeight == MessagingContentProvider.UNSPECIFIED_SIZE) {
                // First get the image data (compressed)
                final ContentResolver cr = mContext.getContentResolver();
                InputStream inputStream = null;
                // Find incoming image size
                try {
                    mOptions.inJustDecodeBounds = true;
                    inputStream = cr.openInputStream(mUri);
                    BitmapFactory.decodeStream(inputStream, null, mOptions);

                    mWidth = mOptions.outWidth;
                    mHeight = mOptions.outHeight;
                    mOptions.inJustDecodeBounds = false;

                    return true;
                } catch (final FileNotFoundException e) {
                    LogUtil.e(TAG, "Could not open file corresponding to uri " + mUri, e);
                } catch (final NullPointerException e) {
                    LogUtil.e(TAG, "NPE trying to open the uri " + mUri, e);
                } finally {
                    if (inputStream != null) {
                        try {
                            inputStream.close();
                        } catch (final IOException e) {
                            // Nothing to do
                        }
                    }
                }

                return false;
            }
            return true;
        }

        /**
         * Choose an initial subsamplesize that ensures the decoded image is no more than
         * MAX_TARGET_SCALE_FACTOR bigger than largest supported image and that it is likely to
         * compress to smaller than the target size (assuming compression down to 1 bit per pixel).
         * @return whether the image can be down subsampled
         */
        private boolean canBeCompressed() {
            final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE);

            int imageHeight = mHeight;
            int imageWidth = mWidth;

            // Assume can use half working memory to decode the initial image (4 bytes per pixel)
            final int workingMemoryPixelLimit = (mMemoryClass * 1024 * 1024 / 8);
            // Target 1 bits per pixel in final compressed image
            final int finalSizePixelLimit = mByteLimit * 8;
            // When choosing to halve the resolution - only do so the image will still be too big
            // after scaling by MAX_TARGET_SCALE_FACTOR
            final int heightLimitWithSlop = (int) (mHeightLimit * MAX_TARGET_SCALE_FACTOR);
            final int widthLimitWithSlop = (int) (mWidthLimit * MAX_TARGET_SCALE_FACTOR);
            final int pixelLimitWithSlop = (int) (finalSizePixelLimit *
                    MAX_TARGET_SCALE_FACTOR * MAX_TARGET_SCALE_FACTOR);
            final int pixelLimit = Math.min(pixelLimitWithSlop, workingMemoryPixelLimit);

            int sampleSize = 1;
            boolean fits = (imageHeight < heightLimitWithSlop &&
                    imageWidth < widthLimitWithSlop &&
                    imageHeight * imageWidth < pixelLimit);

            // Compare sizes to compute sub-sampling needed
            while (!fits) {
                sampleSize = sampleSize * 2;
                // Note that recodeImage may try using mSampleSize * 2. Hence we use the factor of 4
                if (sampleSize >= (Integer.MAX_VALUE / 4)) {
                    LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, String.format(
                            "Cannot resize image: widthLimit=%d heightLimit=%d byteLimit=%d " +
                            "imageWidth=%d imageHeight=%d", mWidthLimit, mHeightLimit, mByteLimit,
                            mWidth, mHeight));
                    Assert.fail("Image cannot be resized"); // http://b/18926934
                    return false;
                }
                if (logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                            "computeInitialSampleSize: Increasing sampleSize to " + sampleSize
                            + " as h=" + imageHeight + " vs " + heightLimitWithSlop
                            + " w=" + imageWidth  + " vs " +  widthLimitWithSlop
                            + " p=" + imageHeight * imageWidth + " vs " + pixelLimit);
                }
                imageHeight = mHeight / sampleSize;
                imageWidth = mWidth / sampleSize;
                fits = (imageHeight < heightLimitWithSlop &&
                        imageWidth < widthLimitWithSlop &&
                        imageHeight * imageWidth < pixelLimit);
            }

            if (logv) {
                LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                        "computeInitialSampleSize: Initial sampleSize " + sampleSize
                        + " for h=" + imageHeight + " vs " + heightLimitWithSlop
                        + " w=" + imageWidth  + " vs " +  widthLimitWithSlop
                        + " p=" + imageHeight * imageWidth + " vs " + pixelLimit);
            }

            mSampleSize = sampleSize;
            return true;
        }

        /**
         * Recode the image from initial Uri to encoded JPEG
         * @param attempt Attempt number
         * @return encoded image
         */
        private byte[] recodeImage(final int attempt) throws FileNotFoundException {
            byte[] encoded = null;
            try {
                final ContentResolver cr = mContext.getContentResolver();
                final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE);
                if (logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: attempt=" + attempt
                            + " limit (w=" + mWidthLimit + " h=" + mHeightLimit + ") quality="
                            + mQuality + " scale=" + mScaleFactor + " sampleSize=" + mSampleSize);
                }
                if (mScaled == null) {
                    if (mDecoded == null) {
                        mOptions.inSampleSize = mSampleSize;
                        try (final InputStream inputStream = cr.openInputStream(mUri)) {
                            mDecoded = BitmapFactory.decodeStream(inputStream, null, mOptions);
                        } catch (IOException e) {
                            // Ignore
                        }
                        if (mDecoded == null) {
                            if (logv) {
                                LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                                        "getResizedImageData: got empty decoded bitmap");
                            }
                            return null;
                        }
                    }
                    if (logv) {
                        LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: decoded w,h="
                                + mDecoded.getWidth() + "," + mDecoded.getHeight());
                    }
                    // Make sure to scale the decoded image if dimension is not within limit
                    final int decodedWidth = mDecoded.getWidth();
                    final int decodedHeight = mDecoded.getHeight();
                    if (decodedWidth > mWidthLimit || decodedHeight > mHeightLimit) {
                        final float minScaleFactor = Math.max(
                                mWidthLimit == 0 ? 1.0f :
                                    (float) decodedWidth / (float) mWidthLimit,
                                    mHeightLimit == 0 ? 1.0f :
                                        (float) decodedHeight / (float) mHeightLimit);
                        if (mScaleFactor < minScaleFactor) {
                            mScaleFactor = minScaleFactor;
                        }
                    }
                    if (mScaleFactor > 1.0 || mOrientationParams.rotation != 0) {
                        mMatrix.reset();
                        mMatrix.postRotate(mOrientationParams.rotation);
                        mMatrix.postScale(mOrientationParams.scaleX / mScaleFactor,
                                mOrientationParams.scaleY / mScaleFactor);
                        mScaled = Bitmap.createBitmap(mDecoded, 0, 0, decodedWidth, decodedHeight,
                                mMatrix, false /* filter */);
                        if (mScaled == null) {
                            if (logv) {
                                LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                                        "getResizedImageData: got empty scaled bitmap");
                            }
                            return null;
                        }
                        if (logv) {
                            LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: scaled w,h="
                                    + mScaled.getWidth() + "," + mScaled.getHeight());
                        }
                    } else {
                        mScaled = mDecoded;
                    }
                }
                // Now encode it at current quality
                encoded = ImageUtils.bitmapToBytes(mScaled, mQuality);
                if (encoded != null && logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                            "getResizedImageData: Encoded down to " + encoded.length + "@"
                                    + mScaled.getWidth() + "/" + mScaled.getHeight() + "~"
                                    + mQuality);
                }
            } catch (final OutOfMemoryError e) {
                LogUtil.w(LogUtil.BUGLE_IMAGE_TAG,
                        "getResizedImageData - image too big (OutOfMemoryError), will try "
                                + " with smaller scale factor");
                // fall through and keep trying with more compression
            }
            return encoded;
        }

        /**
         * When image recode fails this method updates compression parameters for the next attempt
         * @param currentSize encoded image size (will be 0 if OOM)
         */
        private void updateRecodeParameters(final int currentSize) {
            final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE);
            // Only return data within the limit
            if (currentSize > 0 &&
                    mQuality > MINIMUM_IMAGE_COMPRESSION_QUALITY) {
                // First if everything succeeded but failed to hit target size
                // Try quality proportioned to sqrt of size over size limit
                mQuality = Math.max(MINIMUM_IMAGE_COMPRESSION_QUALITY,
                        Math.min((int) (mQuality * Math.sqrt((1.0 * mByteLimit) / currentSize)),
                                (int) (mQuality * QUALITY_SCALE_DOWN_RATIO)));
                if (logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                            "getResizedImageData: Retrying at quality " + mQuality);
                }
            } else if (currentSize > 0 &&
                    mScaleFactor < 2.0 * MIN_SCALE_DOWN_RATIO * MIN_SCALE_DOWN_RATIO) {
                // JPEG compression failed to hit target size - need smaller image
                // First try scaling by a little (< factor of 2) just so long resulting scale down
                // ratio is still significantly bigger than next subsampling step
                // i.e. mScaleFactor/MIN_SCALE_DOWN_RATIO (new scaling factor) <
                //       2.0 / MIN_SCALE_DOWN_RATIO (arbitrary limit)
                mQuality = IMAGE_COMPRESSION_QUALITY;
                mScaleFactor = mScaleFactor / MIN_SCALE_DOWN_RATIO;
                if (logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                            "getResizedImageData: Retrying at scale " + mScaleFactor);
                }
                // Release scaled bitmap to trigger rescaling
                if (mScaled != null && mScaled != mDecoded) {
                    mScaled.recycle();
                }
                mScaled = null;
            } else if (currentSize <= 0 && !mHasReclaimedMemory) {
                // Then before we subsample try cleaning up our cached memory
                Factory.get().reclaimMemory();
                mHasReclaimedMemory = true;
                if (logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                            "getResizedImageData: Retrying after reclaiming memory ");
                }
            } else {
                // Last resort - subsample image by another factor of 2 and try again
                mSampleSize = mSampleSize * 2;
                mQuality = IMAGE_COMPRESSION_QUALITY;
                mScaleFactor = 1.0f;
                if (logv) {
                    LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
                            "getResizedImageData: Retrying at sampleSize " + mSampleSize);
                }
                // Release all bitmaps to trigger subsampling
                if (mScaled != null && mScaled != mDecoded) {
                    mScaled.recycle();
                }
                mScaled = null;
                if (mDecoded != null) {
                    mDecoded.recycle();
                    mDecoded = null;
                }
            }
        }
    }

    /**
     * Scales and center-crops a bitmap to the size passed in and returns the new bitmap.
     *
     * @param source Bitmap to scale and center-crop
     * @param newWidth destination width
     * @param newHeight destination height
     * @return Bitmap scaled and center-cropped bitmap
     */
    public static Bitmap scaleCenterCrop(final Bitmap source, final int newWidth,
            final int newHeight) {
        final int sourceWidth = source.getWidth();
        final int sourceHeight = source.getHeight();

        // Compute the scaling factors to fit the new height and width, respectively.
        // To cover the final image, the final scaling will be the bigger
        // of these two.
        final float xScale = (float) newWidth / sourceWidth;
        final float yScale = (float) newHeight / sourceHeight;
        final float scale = Math.max(xScale, yScale);

        // Now get the size of the source bitmap when scaled
        final float scaledWidth = scale * sourceWidth;
        final float scaledHeight = scale * sourceHeight;

        // Let's find out the upper left coordinates if the scaled bitmap
        // should be centered in the new size give by the parameters
        final float left = (newWidth - scaledWidth) / 2;
        final float top = (newHeight - scaledHeight) / 2;

        // The target rectangle for the new, scaled version of the source bitmap will now
        // be
        final RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);

        // Finally, we create a new bitmap of the specified size and draw our new,
        // scaled bitmap onto it.
        final Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, source.getConfig());
        final Canvas canvas = new Canvas(dest);
        canvas.drawBitmap(source, null, targetRect, null);

        return dest;
    }

    /**
     *  The drawable can be a Nine-Patch. If we directly use the same drawable instance for each
     *  drawable of different sizes, then the drawable sizes would interfere with each other. The
     *  solution here is to create a new drawable instance for every time with the SAME
     *  ConstantState (i.e. sharing the same common state such as the bitmap, so that we don't have
     *  to recreate the bitmap resource), and apply the different properties on top (nine-patch
     *  size and color tint).
     *
     *  TODO: we are creating new drawable instances here, but there are optimizations that
     *  can be made. For example, message bubbles shouldn't need the mutate() call and the
     *  play/pause buttons shouldn't need to create new drawable from the constant state.
     */
    public static Drawable getTintedDrawable(final Context context, final Drawable drawable,
            final int color) {
        // For some reason occassionally drawables on JB has a null constant state
        final Drawable.ConstantState constantStateDrawable = drawable.getConstantState();
        final Drawable retDrawable = (constantStateDrawable != null)
                ? constantStateDrawable.newDrawable(context.getResources()).mutate()
                : drawable;
        retDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
        return retDrawable;
    }

    /**
     * Decodes image resource header and returns the image size.
     */
    public static Rect decodeImageBounds(final Context context, final Uri imageUri) {
        final ContentResolver cr = context.getContentResolver();
        try {
            final InputStream inputStream = cr.openInputStream(imageUri);
            if (inputStream != null) {
                try {
                    BitmapFactory.Options options = new BitmapFactory.Options();
                    options.inJustDecodeBounds = true;
                    BitmapFactory.decodeStream(inputStream, null, options);
                    return new Rect(0, 0, options.outWidth, options.outHeight);
                } finally {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        // Do nothing.
                    }
                }
            }
        } catch (FileNotFoundException e) {
            LogUtil.e(TAG, "Couldn't open input stream for uri = " + imageUri);
        }
        return new Rect(0, 0, ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE);
    }
}
