Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 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 package com.android.messaging.util;
     17 
     18 import android.app.ActivityManager;
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.graphics.Bitmap;
     23 import android.graphics.BitmapFactory;
     24 import android.graphics.BitmapShader;
     25 import android.graphics.Canvas;
     26 import android.graphics.Matrix;
     27 import android.graphics.Paint;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.Rect;
     30 import android.graphics.RectF;
     31 import android.graphics.Shader.TileMode;
     32 import android.graphics.drawable.Drawable;
     33 import android.net.Uri;
     34 import android.provider.MediaStore;
     35 import android.support.annotation.Nullable;
     36 import android.text.TextUtils;
     37 import android.view.View;
     38 
     39 import com.android.messaging.Factory;
     40 import com.android.messaging.datamodel.MediaScratchFileProvider;
     41 import com.android.messaging.datamodel.MessagingContentProvider;
     42 import com.android.messaging.datamodel.media.ImageRequest;
     43 import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
     44 import com.android.messaging.util.exif.ExifInterface;
     45 import com.google.common.annotations.VisibleForTesting;
     46 import com.google.common.io.Files;
     47 
     48 import java.io.ByteArrayOutputStream;
     49 import java.io.File;
     50 import java.io.FileNotFoundException;
     51 import java.io.IOException;
     52 import java.io.InputStream;
     53 import java.nio.charset.Charset;
     54 import java.util.Arrays;
     55 
     56 public class ImageUtils {
     57     private static final String TAG = LogUtil.BUGLE_TAG;
     58     private static final int MAX_OOM_COUNT = 1;
     59     private static final byte[] GIF87_HEADER = "GIF87a".getBytes(Charset.forName("US-ASCII"));
     60     private static final byte[] GIF89_HEADER = "GIF89a".getBytes(Charset.forName("US-ASCII"));
     61 
     62     // Used for drawBitmapWithCircleOnCanvas.
     63     // Default color is transparent for both circle background and stroke.
     64     public static final int DEFAULT_CIRCLE_BACKGROUND_COLOR = 0;
     65     public static final int DEFAULT_CIRCLE_STROKE_COLOR = 0;
     66 
     67     private static volatile ImageUtils sInstance;
     68 
     69     public static ImageUtils get() {
     70         if (sInstance == null) {
     71             synchronized (ImageUtils.class) {
     72                 if (sInstance == null) {
     73                     sInstance = new ImageUtils();
     74                 }
     75             }
     76         }
     77         return sInstance;
     78     }
     79 
     80     @VisibleForTesting
     81     public static void set(final ImageUtils imageUtils) {
     82         sInstance = imageUtils;
     83     }
     84 
     85     /**
     86      * Transforms a bitmap into a byte array.
     87      *
     88      * @param quality Value between 0 and 100 that the compressor uses to discern what quality the
     89      *                resulting bytes should be
     90      * @param bitmap Bitmap to convert into bytes
     91      * @return byte array of bitmap
     92      */
     93     public static byte[] bitmapToBytes(final Bitmap bitmap, final int quality)
     94             throws OutOfMemoryError {
     95         boolean done = false;
     96         int oomCount = 0;
     97         byte[] imageBytes = null;
     98         while (!done) {
     99             try {
    100                 final ByteArrayOutputStream os = new ByteArrayOutputStream();
    101                 bitmap.compress(Bitmap.CompressFormat.JPEG, quality, os);
    102                 imageBytes = os.toByteArray();
    103                 done = true;
    104             } catch (final OutOfMemoryError e) {
    105                 LogUtil.w(TAG, "OutOfMemory converting bitmap to bytes.");
    106                 oomCount++;
    107                 if (oomCount <= MAX_OOM_COUNT) {
    108                     Factory.get().reclaimMemory();
    109                 } else {
    110                     done = true;
    111                     LogUtil.w(TAG, "Failed to convert bitmap to bytes. Out of Memory.");
    112                 }
    113                 throw e;
    114             }
    115         }
    116         return imageBytes;
    117     }
    118 
    119     /**
    120      * Given the source bitmap and a canvas, draws the bitmap through a circular
    121      * mask. Only draws a circle with diameter equal to the destination width.
    122      *
    123      * @param bitmap The source bitmap to draw.
    124      * @param canvas The canvas to draw it on.
    125      * @param source The source bound of the bitmap.
    126      * @param dest The destination bound on the canvas.
    127      * @param bitmapPaint Optional Paint object for the bitmap
    128      * @param fillBackground when set, fill the circle with backgroundColor
    129      * @param strokeColor draw a border outside the circle with strokeColor
    130      */
    131     public static void drawBitmapWithCircleOnCanvas(final Bitmap bitmap, final Canvas canvas,
    132             final RectF source, final RectF dest, @Nullable Paint bitmapPaint,
    133             final boolean fillBackground, final int backgroundColor, int strokeColor) {
    134         // Draw bitmap through shader first.
    135         final BitmapShader shader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP);
    136         final Matrix matrix = new Matrix();
    137 
    138         // Fit bitmap to bounds.
    139         matrix.setRectToRect(source, dest, Matrix.ScaleToFit.CENTER);
    140 
    141         shader.setLocalMatrix(matrix);
    142 
    143         if (bitmapPaint == null) {
    144             bitmapPaint = new Paint();
    145         }
    146 
    147         bitmapPaint.setAntiAlias(true);
    148         if (fillBackground) {
    149             bitmapPaint.setColor(backgroundColor);
    150             canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint);
    151         }
    152 
    153         bitmapPaint.setShader(shader);
    154         canvas.drawCircle(dest.centerX(), dest.centerX(), dest.width() / 2f, bitmapPaint);
    155         bitmapPaint.setShader(null);
    156 
    157         if (strokeColor != 0) {
    158             final Paint stroke = new Paint();
    159             stroke.setAntiAlias(true);
    160             stroke.setColor(strokeColor);
    161             stroke.setStyle(Paint.Style.STROKE);
    162             final float strokeWidth = 6f;
    163             stroke.setStrokeWidth(strokeWidth);
    164             canvas.drawCircle(dest.centerX(),
    165                     dest.centerX(),
    166                     dest.width() / 2f - stroke.getStrokeWidth() / 2f,
    167                     stroke);
    168         }
    169     }
    170 
    171     /**
    172      * Sets a drawable to the background of a view. setBackgroundDrawable() is deprecated since
    173      * JB and replaced by setBackground().
    174      */
    175     @SuppressWarnings("deprecation")
    176     public static void setBackgroundDrawableOnView(final View view, final Drawable drawable) {
    177         if (OsUtil.isAtLeastJB()) {
    178             view.setBackground(drawable);
    179         } else {
    180             view.setBackgroundDrawable(drawable);
    181         }
    182     }
    183 
    184     /**
    185      * Based on the input bitmap bounds given by BitmapFactory.Options, compute the required
    186      * sub-sampling size for loading a scaled down version of the bitmap to the required size
    187      * @param options a BitmapFactory.Options instance containing the bounds info of the bitmap
    188      * @param reqWidth the desired width of the bitmap. Can be ImageRequest.UNSPECIFIED_SIZE.
    189      * @param reqHeight the desired height of the bitmap.  Can be ImageRequest.UNSPECIFIED_SIZE.
    190      * @return
    191      */
    192     public int calculateInSampleSize(
    193             final BitmapFactory.Options options, final int reqWidth, final int reqHeight) {
    194         // Raw height and width of image
    195         final int height = options.outHeight;
    196         final int width = options.outWidth;
    197         int inSampleSize = 1;
    198 
    199         final boolean checkHeight = reqHeight != ImageRequest.UNSPECIFIED_SIZE;
    200         final boolean checkWidth = reqWidth != ImageRequest.UNSPECIFIED_SIZE;
    201         if ((checkHeight && height > reqHeight) ||
    202                 (checkWidth && width > reqWidth)) {
    203 
    204             final int halfHeight = height / 2;
    205             final int halfWidth = width / 2;
    206 
    207             // Calculate the largest inSampleSize value that is a power of 2 and keeps both
    208             // height and width larger than the requested height and width.
    209             while ((!checkHeight || (halfHeight / inSampleSize) > reqHeight)
    210                     && (!checkWidth || (halfWidth / inSampleSize) > reqWidth)) {
    211                 inSampleSize *= 2;
    212             }
    213         }
    214 
    215         return inSampleSize;
    216     }
    217 
    218     private static final String[] MEDIA_CONTENT_PROJECTION = new String[] {
    219         MediaStore.MediaColumns.MIME_TYPE
    220     };
    221 
    222     private static final int INDEX_CONTENT_TYPE = 0;
    223 
    224     @DoesNotRunOnMainThread
    225     public static String getContentType(final ContentResolver cr, final Uri uri) {
    226         // Figure out the content type of media.
    227         String contentType = null;
    228         Cursor cursor = null;
    229         if (UriUtil.isMediaStoreUri(uri)) {
    230             try {
    231                 cursor = cr.query(uri, MEDIA_CONTENT_PROJECTION, null, null, null);
    232 
    233                 if (cursor != null && cursor.moveToFirst()) {
    234                     contentType = cursor.getString(INDEX_CONTENT_TYPE);
    235                 }
    236             } finally {
    237                 if (cursor != null) {
    238                     cursor.close();
    239                 }
    240             }
    241         }
    242         if (contentType == null) {
    243             // Last ditch effort to get the content type. Look at the file extension.
    244             contentType = ContentType.getContentTypeFromExtension(uri.toString(),
    245                     ContentType.IMAGE_UNSPECIFIED);
    246         }
    247         return contentType;
    248     }
    249 
    250     /**
    251      * @param context Android context
    252      * @param uri Uri to the image data
    253      * @return The exif orientation value for the image in the specified uri
    254      */
    255     public static int getOrientation(final Context context, final Uri uri) {
    256         try {
    257             return getOrientation(context.getContentResolver().openInputStream(uri));
    258         } catch (FileNotFoundException e) {
    259             LogUtil.e(TAG, "getOrientation couldn't open: " + uri, e);
    260         }
    261         return android.media.ExifInterface.ORIENTATION_UNDEFINED;
    262     }
    263 
    264     /**
    265      * @param inputStream The stream to the image file.  Closed on completion
    266      * @return The exif orientation value for the image in the specified stream
    267      */
    268     public static int getOrientation(final InputStream inputStream) {
    269         int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
    270         if (inputStream != null) {
    271             try {
    272                 final ExifInterface exifInterface = new ExifInterface();
    273                 exifInterface.readExif(inputStream);
    274                 final Integer orientationValue =
    275                         exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
    276                 if (orientationValue != null) {
    277                     orientation = orientationValue.intValue();
    278                 }
    279             } catch (IOException e) {
    280                 // If the image if GIF, PNG, or missing exif header, just use the defaults
    281             } finally {
    282                 try {
    283                     if (inputStream != null) {
    284                         inputStream.close();
    285                     }
    286                 } catch (IOException e) {
    287                     LogUtil.e(TAG, "getOrientation error closing input stream", e);
    288                 }
    289             }
    290         }
    291         return orientation;
    292     }
    293 
    294     /**
    295      * Returns whether the resource is a GIF image.
    296      */
    297     public static boolean isGif(String contentType, Uri contentUri) {
    298         if (TextUtils.equals(contentType, ContentType.IMAGE_GIF)) {
    299             return true;
    300         }
    301         if (ContentType.isImageType(contentType)) {
    302             try {
    303                 ContentResolver contentResolver = Factory.get().getApplicationContext()
    304                         .getContentResolver();
    305                 InputStream inputStream = contentResolver.openInputStream(contentUri);
    306                 return ImageUtils.isGif(inputStream);
    307             } catch (Exception e) {
    308                 LogUtil.w(TAG, "Could not open GIF input stream", e);
    309             }
    310         }
    311         // Assume anything with a non-image content type is not a GIF
    312         return false;
    313     }
    314 
    315     /**
    316      * @param inputStream The stream to the image file. Closed on completion
    317      * @return Whether the image stream represents a GIF
    318      */
    319     public static boolean isGif(InputStream inputStream) {
    320         if (inputStream != null) {
    321             try {
    322                 byte[] gifHeaderBytes = new byte[6];
    323                 int value = inputStream.read(gifHeaderBytes, 0, 6);
    324                 if (value == 6) {
    325                     return Arrays.equals(gifHeaderBytes, GIF87_HEADER)
    326                             || Arrays.equals(gifHeaderBytes, GIF89_HEADER);
    327                 }
    328             } catch (IOException e) {
    329                 return false;
    330             } finally {
    331                 try {
    332                     inputStream.close();
    333                 } catch (IOException e) {
    334                     // Ignore
    335                 }
    336             }
    337         }
    338         return false;
    339     }
    340 
    341     /**
    342      * Read an image and compress it to particular max dimensions and size.
    343      * Used to ensure images can fit in an MMS.
    344      * TODO: This uses memory very inefficiently as it processes the whole image as a unit
    345      *  (rather than slice by slice) but system JPEG functions do not support slicing and dicing.
    346      */
    347     public static class ImageResizer {
    348 
    349         /**
    350          * The quality parameter which is used to compress JPEG images.
    351          */
    352         private static final int IMAGE_COMPRESSION_QUALITY = 95;
    353         /**
    354          * The minimum quality parameter which is used to compress JPEG images.
    355          */
    356         private static final int MINIMUM_IMAGE_COMPRESSION_QUALITY = 50;
    357 
    358         /**
    359          * Minimum factor to reduce quality value
    360          */
    361         private static final double QUALITY_SCALE_DOWN_RATIO = 0.85f;
    362 
    363         /**
    364          * Maximum passes through the resize loop before failing permanently
    365          */
    366         private static final int NUMBER_OF_RESIZE_ATTEMPTS = 6;
    367 
    368         /**
    369          * Amount to scale down the picture when it doesn't fit
    370          */
    371         private static final float MIN_SCALE_DOWN_RATIO = 0.75f;
    372 
    373         /**
    374          * When computing sampleSize target scaling of no more than this ratio
    375          */
    376         private static final float MAX_TARGET_SCALE_FACTOR = 1.5f;
    377 
    378 
    379         // Current sample size for subsampling image during initial decode
    380         private int mSampleSize;
    381         // Current bitmap holding initial decoded source image
    382         private Bitmap mDecoded;
    383         // If scaling is needed this holds the scaled bitmap (else should equal mDecoded)
    384         private Bitmap mScaled;
    385         // Current JPEG compression quality to use when compressing image
    386         private int mQuality;
    387         // Current factor to scale down decoded image before compressing
    388         private float mScaleFactor;
    389         // Flag keeping track of whether cache memory has been reclaimed
    390         private boolean mHasReclaimedMemory;
    391 
    392         // Initial size of the image (typically provided but can be UNSPECIFIED_SIZE)
    393         private int mWidth;
    394         private int mHeight;
    395         // Orientation params of image as read from EXIF data
    396         private final ExifInterface.OrientationParams mOrientationParams;
    397         // Matrix to undo orientation and scale at the same time
    398         private final Matrix mMatrix;
    399         // Size limit as provided by MMS library
    400         private final int mWidthLimit;
    401         private final int mHeightLimit;
    402         private final int mByteLimit;
    403         //  Uri from which to read source image
    404         private final Uri mUri;
    405         // Application context
    406         private final Context mContext;
    407         // Cached value of bitmap factory options
    408         private final BitmapFactory.Options mOptions;
    409         private final String mContentType;
    410 
    411         private final int mMemoryClass;
    412 
    413         /**
    414          * Return resized (compressed) image (else null)
    415          *
    416          * @param width The width of the image (if known)
    417          * @param height The height of the image (if known)
    418          * @param orientation The orientation of the image as an ExifInterface constant
    419          * @param widthLimit The width limit, in pixels
    420          * @param heightLimit The height limit, in pixels
    421          * @param byteLimit The binary size limit, in bytes
    422          * @param uri Uri to the image data
    423          * @param context Needed to open the image
    424          * @param contentType of image
    425          * @return encoded image meeting size requirements else null
    426          */
    427         public static byte[] getResizedImageData(final int width, final int height,
    428                 final int orientation, final int widthLimit, final int heightLimit,
    429                 final int byteLimit, final Uri uri, final Context context,
    430                 final String contentType) {
    431             final ImageResizer resizer = new ImageResizer(width, height, orientation,
    432                     widthLimit, heightLimit, byteLimit, uri, context, contentType);
    433             return resizer.resize();
    434         }
    435 
    436         /**
    437          * Create and initialize an image resizer
    438          */
    439         private ImageResizer(final int width, final int height, final int orientation,
    440                 final int widthLimit, final int heightLimit, final int byteLimit, final Uri uri,
    441                 final Context context, final String contentType) {
    442             mWidth = width;
    443             mHeight = height;
    444             mOrientationParams = ExifInterface.getOrientationParams(orientation);
    445             mMatrix = new Matrix();
    446             mWidthLimit = widthLimit;
    447             mHeightLimit = heightLimit;
    448             mByteLimit = byteLimit;
    449             mUri = uri;
    450             mWidth = width;
    451             mContext = context;
    452             mQuality = IMAGE_COMPRESSION_QUALITY;
    453             mScaleFactor = 1.0f;
    454             mHasReclaimedMemory = false;
    455             mOptions = new BitmapFactory.Options();
    456             mOptions.inScaled = false;
    457             mOptions.inDensity = 0;
    458             mOptions.inTargetDensity = 0;
    459             mOptions.inSampleSize = 1;
    460             mOptions.inJustDecodeBounds = false;
    461             mOptions.inMutable = false;
    462             final ActivityManager am =
    463                     (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    464             mMemoryClass = Math.max(16, am.getMemoryClass());
    465             mContentType = contentType;
    466         }
    467 
    468         /**
    469          * Try to compress the image
    470          *
    471          * @return encoded image meeting size requirements else null
    472          */
    473         private byte[] resize() {
    474             return ImageUtils.isGif(mContentType, mUri) ? resizeGifImage() : resizeStaticImage();
    475         }
    476 
    477         private byte[] resizeGifImage() {
    478             byte[] bytesToReturn = null;
    479             final String inputFilePath;
    480             if (MediaScratchFileProvider.isMediaScratchSpaceUri(mUri)) {
    481                 inputFilePath = MediaScratchFileProvider.getFileFromUri(mUri).getAbsolutePath();
    482             } else {
    483                 if (!TextUtils.equals(mUri.getScheme(), ContentResolver.SCHEME_FILE)) {
    484                     Assert.fail("Expected a GIF file uri, but actual uri = " + mUri.toString());
    485                 }
    486                 inputFilePath = mUri.getPath();
    487             }
    488 
    489             if (GifTranscoder.canBeTranscoded(mWidth, mHeight)) {
    490                 // Needed to perform the transcoding so that the gif can continue to play in the
    491                 // conversation while the sending is taking place
    492                 final Uri tmpUri = MediaScratchFileProvider.buildMediaScratchSpaceUri("gif");
    493                 final File outputFile = MediaScratchFileProvider.getFileFromUri(tmpUri);
    494                 final String outputFilePath = outputFile.getAbsolutePath();
    495 
    496                 final boolean success =
    497                         GifTranscoder.transcode(mContext, inputFilePath, outputFilePath);
    498                 if (success) {
    499                     try {
    500                         bytesToReturn = Files.toByteArray(outputFile);
    501                     } catch (IOException e) {
    502                         LogUtil.e(TAG, "Could not create FileInputStream with path of "
    503                                 + outputFilePath, e);
    504                     }
    505                 }
    506 
    507                 // Need to clean up the new file created to compress the gif
    508                 mContext.getContentResolver().delete(tmpUri, null, null);
    509             } else {
    510                 // We don't want to transcode the gif because its image dimensions would be too
    511                 // small so just return the bytes of the original gif
    512                 try {
    513                     bytesToReturn = Files.toByteArray(new File(inputFilePath));
    514                 } catch (IOException e) {
    515                     LogUtil.e(TAG,
    516                             "Could not create FileInputStream with path of " + inputFilePath, e);
    517                 }
    518             }
    519 
    520             return bytesToReturn;
    521         }
    522 
    523         private byte[] resizeStaticImage() {
    524             if (!ensureImageSizeSet()) {
    525                 // Cannot read image size
    526                 return null;
    527             }
    528             // Find incoming image size
    529             if (!canBeCompressed()) {
    530                 return null;
    531             }
    532 
    533             //  Decode image - if out of memory - reclaim memory and retry
    534             try {
    535                 for (int attempts = 0; attempts < NUMBER_OF_RESIZE_ATTEMPTS; attempts++) {
    536                     final byte[] encoded = recodeImage(attempts);
    537 
    538                     // Only return data within the limit
    539                     if (encoded != null && encoded.length <= mByteLimit) {
    540                         return encoded;
    541                     } else {
    542                         final int currentSize = (encoded == null ? 0 : encoded.length);
    543                         updateRecodeParameters(currentSize);
    544                     }
    545                 }
    546             } catch (final FileNotFoundException e) {
    547                 LogUtil.e(TAG, "File disappeared during resizing");
    548             } finally {
    549                 // Release all bitmaps
    550                 if (mScaled != null && mScaled != mDecoded) {
    551                     mScaled.recycle();
    552                 }
    553                 if (mDecoded != null) {
    554                     mDecoded.recycle();
    555                 }
    556             }
    557             return null;
    558         }
    559 
    560         /**
    561          * Ensure that the width and height of the source image are known
    562          * @return flag indicating whether size is known
    563          */
    564         private boolean ensureImageSizeSet() {
    565             if (mWidth == MessagingContentProvider.UNSPECIFIED_SIZE ||
    566                     mHeight == MessagingContentProvider.UNSPECIFIED_SIZE) {
    567                 // First get the image data (compressed)
    568                 final ContentResolver cr = mContext.getContentResolver();
    569                 InputStream inputStream = null;
    570                 // Find incoming image size
    571                 try {
    572                     mOptions.inJustDecodeBounds = true;
    573                     inputStream = cr.openInputStream(mUri);
    574                     BitmapFactory.decodeStream(inputStream, null, mOptions);
    575 
    576                     mWidth = mOptions.outWidth;
    577                     mHeight = mOptions.outHeight;
    578                     mOptions.inJustDecodeBounds = false;
    579 
    580                     return true;
    581                 } catch (final FileNotFoundException e) {
    582                     LogUtil.e(TAG, "Could not open file corresponding to uri " + mUri, e);
    583                 } catch (final NullPointerException e) {
    584                     LogUtil.e(TAG, "NPE trying to open the uri " + mUri, e);
    585                 } finally {
    586                     if (inputStream != null) {
    587                         try {
    588                             inputStream.close();
    589                         } catch (final IOException e) {
    590                             // Nothing to do
    591                         }
    592                     }
    593                 }
    594 
    595                 return false;
    596             }
    597             return true;
    598         }
    599 
    600         /**
    601          * Choose an initial subsamplesize that ensures the decoded image is no more than
    602          * MAX_TARGET_SCALE_FACTOR bigger than largest supported image and that it is likely to
    603          * compress to smaller than the target size (assuming compression down to 1 bit per pixel).
    604          * @return whether the image can be down subsampled
    605          */
    606         private boolean canBeCompressed() {
    607             final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE);
    608 
    609             int imageHeight = mHeight;
    610             int imageWidth = mWidth;
    611 
    612             // Assume can use half working memory to decode the initial image (4 bytes per pixel)
    613             final int workingMemoryPixelLimit = (mMemoryClass * 1024 * 1024 / 8);
    614             // Target 1 bits per pixel in final compressed image
    615             final int finalSizePixelLimit = mByteLimit * 8;
    616             // When choosing to halve the resolution - only do so the image will still be too big
    617             // after scaling by MAX_TARGET_SCALE_FACTOR
    618             final int heightLimitWithSlop = (int) (mHeightLimit * MAX_TARGET_SCALE_FACTOR);
    619             final int widthLimitWithSlop = (int) (mWidthLimit * MAX_TARGET_SCALE_FACTOR);
    620             final int pixelLimitWithSlop = (int) (finalSizePixelLimit *
    621                     MAX_TARGET_SCALE_FACTOR * MAX_TARGET_SCALE_FACTOR);
    622             final int pixelLimit = Math.min(pixelLimitWithSlop, workingMemoryPixelLimit);
    623 
    624             int sampleSize = 1;
    625             boolean fits = (imageHeight < heightLimitWithSlop &&
    626                     imageWidth < widthLimitWithSlop &&
    627                     imageHeight * imageWidth < pixelLimit);
    628 
    629             // Compare sizes to compute sub-sampling needed
    630             while (!fits) {
    631                 sampleSize = sampleSize * 2;
    632                 // Note that recodeImage may try using mSampleSize * 2. Hence we use the factor of 4
    633                 if (sampleSize >= (Integer.MAX_VALUE / 4)) {
    634                     LogUtil.w(LogUtil.BUGLE_IMAGE_TAG, String.format(
    635                             "Cannot resize image: widthLimit=%d heightLimit=%d byteLimit=%d " +
    636                             "imageWidth=%d imageHeight=%d", mWidthLimit, mHeightLimit, mByteLimit,
    637                             mWidth, mHeight));
    638                     Assert.fail("Image cannot be resized"); // http://b/18926934
    639                     return false;
    640                 }
    641                 if (logv) {
    642                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    643                             "computeInitialSampleSize: Increasing sampleSize to " + sampleSize
    644                             + " as h=" + imageHeight + " vs " + heightLimitWithSlop
    645                             + " w=" + imageWidth  + " vs " +  widthLimitWithSlop
    646                             + " p=" + imageHeight * imageWidth + " vs " + pixelLimit);
    647                 }
    648                 imageHeight = mHeight / sampleSize;
    649                 imageWidth = mWidth / sampleSize;
    650                 fits = (imageHeight < heightLimitWithSlop &&
    651                         imageWidth < widthLimitWithSlop &&
    652                         imageHeight * imageWidth < pixelLimit);
    653             }
    654 
    655             if (logv) {
    656                 LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    657                         "computeInitialSampleSize: Initial sampleSize " + sampleSize
    658                         + " for h=" + imageHeight + " vs " + heightLimitWithSlop
    659                         + " w=" + imageWidth  + " vs " +  widthLimitWithSlop
    660                         + " p=" + imageHeight * imageWidth + " vs " + pixelLimit);
    661             }
    662 
    663             mSampleSize = sampleSize;
    664             return true;
    665         }
    666 
    667         /**
    668          * Recode the image from initial Uri to encoded JPEG
    669          * @param attempt Attempt number
    670          * @return encoded image
    671          */
    672         private byte[] recodeImage(final int attempt) throws FileNotFoundException {
    673             byte[] encoded = null;
    674             try {
    675                 final ContentResolver cr = mContext.getContentResolver();
    676                 final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE);
    677                 if (logv) {
    678                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: attempt=" + attempt
    679                             + " limit (w=" + mWidthLimit + " h=" + mHeightLimit + ") quality="
    680                             + mQuality + " scale=" + mScaleFactor + " sampleSize=" + mSampleSize);
    681                 }
    682                 if (mScaled == null) {
    683                     if (mDecoded == null) {
    684                         mOptions.inSampleSize = mSampleSize;
    685                         final InputStream inputStream = cr.openInputStream(mUri);
    686                         mDecoded = BitmapFactory.decodeStream(inputStream, null, mOptions);
    687                         if (mDecoded == null) {
    688                             if (logv) {
    689                                 LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    690                                         "getResizedImageData: got empty decoded bitmap");
    691                             }
    692                             return null;
    693                         }
    694                     }
    695                     if (logv) {
    696                         LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: decoded w,h="
    697                                 + mDecoded.getWidth() + "," + mDecoded.getHeight());
    698                     }
    699                     // Make sure to scale the decoded image if dimension is not within limit
    700                     final int decodedWidth = mDecoded.getWidth();
    701                     final int decodedHeight = mDecoded.getHeight();
    702                     if (decodedWidth > mWidthLimit || decodedHeight > mHeightLimit) {
    703                         final float minScaleFactor = Math.max(
    704                                 mWidthLimit == 0 ? 1.0f :
    705                                     (float) decodedWidth / (float) mWidthLimit,
    706                                     mHeightLimit == 0 ? 1.0f :
    707                                         (float) decodedHeight / (float) mHeightLimit);
    708                         if (mScaleFactor < minScaleFactor) {
    709                             mScaleFactor = minScaleFactor;
    710                         }
    711                     }
    712                     if (mScaleFactor > 1.0 || mOrientationParams.rotation != 0) {
    713                         mMatrix.reset();
    714                         mMatrix.postRotate(mOrientationParams.rotation);
    715                         mMatrix.postScale(mOrientationParams.scaleX / mScaleFactor,
    716                                 mOrientationParams.scaleY / mScaleFactor);
    717                         mScaled = Bitmap.createBitmap(mDecoded, 0, 0, decodedWidth, decodedHeight,
    718                                 mMatrix, false /* filter */);
    719                         if (mScaled == null) {
    720                             if (logv) {
    721                                 LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    722                                         "getResizedImageData: got empty scaled bitmap");
    723                             }
    724                             return null;
    725                         }
    726                         if (logv) {
    727                             LogUtil.v(LogUtil.BUGLE_IMAGE_TAG, "getResizedImageData: scaled w,h="
    728                                     + mScaled.getWidth() + "," + mScaled.getHeight());
    729                         }
    730                     } else {
    731                         mScaled = mDecoded;
    732                     }
    733                 }
    734                 // Now encode it at current quality
    735                 encoded = ImageUtils.bitmapToBytes(mScaled, mQuality);
    736                 if (encoded != null && logv) {
    737                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    738                             "getResizedImageData: Encoded down to " + encoded.length + "@"
    739                                     + mScaled.getWidth() + "/" + mScaled.getHeight() + "~"
    740                                     + mQuality);
    741                 }
    742             } catch (final OutOfMemoryError e) {
    743                 LogUtil.w(LogUtil.BUGLE_IMAGE_TAG,
    744                         "getResizedImageData - image too big (OutOfMemoryError), will try "
    745                                 + " with smaller scale factor");
    746                 // fall through and keep trying with more compression
    747             }
    748             return encoded;
    749         }
    750 
    751         /**
    752          * When image recode fails this method updates compression parameters for the next attempt
    753          * @param currentSize encoded image size (will be 0 if OOM)
    754          */
    755         private void updateRecodeParameters(final int currentSize) {
    756             final boolean logv = LogUtil.isLoggable(LogUtil.BUGLE_IMAGE_TAG, LogUtil.VERBOSE);
    757             // Only return data within the limit
    758             if (currentSize > 0 &&
    759                     mQuality > MINIMUM_IMAGE_COMPRESSION_QUALITY) {
    760                 // First if everything succeeded but failed to hit target size
    761                 // Try quality proportioned to sqrt of size over size limit
    762                 mQuality = Math.max(MINIMUM_IMAGE_COMPRESSION_QUALITY,
    763                         Math.min((int) (mQuality * Math.sqrt((1.0 * mByteLimit) / currentSize)),
    764                                 (int) (mQuality * QUALITY_SCALE_DOWN_RATIO)));
    765                 if (logv) {
    766                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    767                             "getResizedImageData: Retrying at quality " + mQuality);
    768                 }
    769             } else if (currentSize > 0 &&
    770                     mScaleFactor < 2.0 * MIN_SCALE_DOWN_RATIO * MIN_SCALE_DOWN_RATIO) {
    771                 // JPEG compression failed to hit target size - need smaller image
    772                 // First try scaling by a little (< factor of 2) just so long resulting scale down
    773                 // ratio is still significantly bigger than next subsampling step
    774                 // i.e. mScaleFactor/MIN_SCALE_DOWN_RATIO (new scaling factor) <
    775                 //       2.0 / MIN_SCALE_DOWN_RATIO (arbitrary limit)
    776                 mQuality = IMAGE_COMPRESSION_QUALITY;
    777                 mScaleFactor = mScaleFactor / MIN_SCALE_DOWN_RATIO;
    778                 if (logv) {
    779                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    780                             "getResizedImageData: Retrying at scale " + mScaleFactor);
    781                 }
    782                 // Release scaled bitmap to trigger rescaling
    783                 if (mScaled != null && mScaled != mDecoded) {
    784                     mScaled.recycle();
    785                 }
    786                 mScaled = null;
    787             } else if (currentSize <= 0 && !mHasReclaimedMemory) {
    788                 // Then before we subsample try cleaning up our cached memory
    789                 Factory.get().reclaimMemory();
    790                 mHasReclaimedMemory = true;
    791                 if (logv) {
    792                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    793                             "getResizedImageData: Retrying after reclaiming memory ");
    794                 }
    795             } else {
    796                 // Last resort - subsample image by another factor of 2 and try again
    797                 mSampleSize = mSampleSize * 2;
    798                 mQuality = IMAGE_COMPRESSION_QUALITY;
    799                 mScaleFactor = 1.0f;
    800                 if (logv) {
    801                     LogUtil.v(LogUtil.BUGLE_IMAGE_TAG,
    802                             "getResizedImageData: Retrying at sampleSize " + mSampleSize);
    803                 }
    804                 // Release all bitmaps to trigger subsampling
    805                 if (mScaled != null && mScaled != mDecoded) {
    806                     mScaled.recycle();
    807                 }
    808                 mScaled = null;
    809                 if (mDecoded != null) {
    810                     mDecoded.recycle();
    811                     mDecoded = null;
    812                 }
    813             }
    814         }
    815     }
    816 
    817     /**
    818      * Scales and center-crops a bitmap to the size passed in and returns the new bitmap.
    819      *
    820      * @param source Bitmap to scale and center-crop
    821      * @param newWidth destination width
    822      * @param newHeight destination height
    823      * @return Bitmap scaled and center-cropped bitmap
    824      */
    825     public static Bitmap scaleCenterCrop(final Bitmap source, final int newWidth,
    826             final int newHeight) {
    827         final int sourceWidth = source.getWidth();
    828         final int sourceHeight = source.getHeight();
    829 
    830         // Compute the scaling factors to fit the new height and width, respectively.
    831         // To cover the final image, the final scaling will be the bigger
    832         // of these two.
    833         final float xScale = (float) newWidth / sourceWidth;
    834         final float yScale = (float) newHeight / sourceHeight;
    835         final float scale = Math.max(xScale, yScale);
    836 
    837         // Now get the size of the source bitmap when scaled
    838         final float scaledWidth = scale * sourceWidth;
    839         final float scaledHeight = scale * sourceHeight;
    840 
    841         // Let's find out the upper left coordinates if the scaled bitmap
    842         // should be centered in the new size give by the parameters
    843         final float left = (newWidth - scaledWidth) / 2;
    844         final float top = (newHeight - scaledHeight) / 2;
    845 
    846         // The target rectangle for the new, scaled version of the source bitmap will now
    847         // be
    848         final RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
    849 
    850         // Finally, we create a new bitmap of the specified size and draw our new,
    851         // scaled bitmap onto it.
    852         final Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, source.getConfig());
    853         final Canvas canvas = new Canvas(dest);
    854         canvas.drawBitmap(source, null, targetRect, null);
    855 
    856         return dest;
    857     }
    858 
    859     /**
    860      *  The drawable can be a Nine-Patch. If we directly use the same drawable instance for each
    861      *  drawable of different sizes, then the drawable sizes would interfere with each other. The
    862      *  solution here is to create a new drawable instance for every time with the SAME
    863      *  ConstantState (i.e. sharing the same common state such as the bitmap, so that we don't have
    864      *  to recreate the bitmap resource), and apply the different properties on top (nine-patch
    865      *  size and color tint).
    866      *
    867      *  TODO: we are creating new drawable instances here, but there are optimizations that
    868      *  can be made. For example, message bubbles shouldn't need the mutate() call and the
    869      *  play/pause buttons shouldn't need to create new drawable from the constant state.
    870      */
    871     public static Drawable getTintedDrawable(final Context context, final Drawable drawable,
    872             final int color) {
    873         // For some reason occassionally drawables on JB has a null constant state
    874         final Drawable.ConstantState constantStateDrawable = drawable.getConstantState();
    875         final Drawable retDrawable = (constantStateDrawable != null)
    876                 ? constantStateDrawable.newDrawable(context.getResources()).mutate()
    877                 : drawable;
    878         retDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
    879         return retDrawable;
    880     }
    881 
    882     /**
    883      * Decodes image resource header and returns the image size.
    884      */
    885     public static Rect decodeImageBounds(final Context context, final Uri imageUri) {
    886         final ContentResolver cr = context.getContentResolver();
    887         try {
    888             final InputStream inputStream = cr.openInputStream(imageUri);
    889             if (inputStream != null) {
    890                 try {
    891                     BitmapFactory.Options options = new BitmapFactory.Options();
    892                     options.inJustDecodeBounds = true;
    893                     BitmapFactory.decodeStream(inputStream, null, options);
    894                     return new Rect(0, 0, options.outWidth, options.outHeight);
    895                 } finally {
    896                     try {
    897                         inputStream.close();
    898                     } catch (IOException e) {
    899                         // Do nothing.
    900                     }
    901                 }
    902             }
    903         } catch (FileNotFoundException e) {
    904             LogUtil.e(TAG, "Couldn't open input stream for uri = " + imageUri);
    905         }
    906         return new Rect(0, 0, ImageRequest.UNSPECIFIED_SIZE, ImageRequest.UNSPECIFIED_SIZE);
    907     }
    908 }
    909