Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2008 Esmertec AG.
      3  * Copyright (C) 2008 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mms.ui;
     19 
     20 import com.android.mms.model.ImageModel;
     21 import com.android.mms.LogTag;
     22 
     23 import com.google.android.mms.ContentType;
     24 import com.google.android.mms.pdu.PduPart;
     25 import android.database.sqlite.SqliteWrapper;
     26 
     27 import android.content.Context;
     28 import android.database.Cursor;
     29 import android.graphics.Bitmap;
     30 import android.graphics.BitmapFactory;
     31 import android.graphics.Bitmap.CompressFormat;
     32 import android.net.Uri;
     33 import android.provider.MediaStore.Images;
     34 import android.provider.Telephony.Mms.Part;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 import android.webkit.MimeTypeMap;
     38 
     39 import java.io.ByteArrayOutputStream;
     40 import java.io.FileNotFoundException;
     41 import java.io.IOException;
     42 import java.io.InputStream;
     43 import java.io.OutputStream;
     44 
     45 public class UriImage {
     46     private static final String TAG = "Mms/image";
     47     private static final boolean DEBUG = false;
     48     private static final boolean LOCAL_LOGV = false;
     49 
     50     private final Context mContext;
     51     private final Uri mUri;
     52     private String mContentType;
     53     private String mPath;
     54     private String mSrc;
     55     private int mWidth;
     56     private int mHeight;
     57 
     58     public UriImage(Context context, Uri uri) {
     59         if ((null == context) || (null == uri)) {
     60             throw new IllegalArgumentException();
     61         }
     62 
     63         String scheme = uri.getScheme();
     64         if (scheme.equals("content")) {
     65             initFromContentUri(context, uri);
     66         } else if (uri.getScheme().equals("file")) {
     67             initFromFile(context, uri);
     68         }
     69 
     70         mContext = context;
     71         mUri = uri;
     72 
     73         decodeBoundsInfo();
     74 
     75         if (LOCAL_LOGV) {
     76             Log.v(TAG, "UriImage uri: " + uri + " mPath: " + mPath + " mWidth: " + mWidth +
     77                     " mHeight: " + mHeight);
     78         }
     79     }
     80 
     81     private void initFromFile(Context context, Uri uri) {
     82         mPath = uri.getPath();
     83         MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
     84         String extension = MimeTypeMap.getFileExtensionFromUrl(mPath);
     85         if (TextUtils.isEmpty(extension)) {
     86             // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle
     87             // urlEncoded strings. Let's try one last time at finding the extension.
     88             int dotPos = mPath.lastIndexOf('.');
     89             if (0 <= dotPos) {
     90                 extension = mPath.substring(dotPos + 1);
     91             }
     92         }
     93         mContentType = mimeTypeMap.getMimeTypeFromExtension(extension);
     94         // It's ok if mContentType is null. Eventually we'll show a toast telling the
     95         // user the picture couldn't be attached.
     96 
     97         buildSrcFromPath();
     98     }
     99 
    100     private void buildSrcFromPath() {
    101         mSrc = mPath.substring(mPath.lastIndexOf('/') + 1);
    102 
    103         if(mSrc.startsWith(".") && mSrc.length() > 1) {
    104             mSrc = mSrc.substring(1);
    105         }
    106 
    107         // Some MMSCs appear to have problems with filenames
    108         // containing a space.  So just replace them with
    109         // underscores in the name, which is typically not
    110         // visible to the user anyway.
    111         mSrc = mSrc.replace(' ', '_');
    112     }
    113 
    114     private void initFromContentUri(Context context, Uri uri) {
    115         Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
    116                             uri, null, null, null, null);
    117 
    118         mSrc = null;
    119         if (c == null) {
    120             throw new IllegalArgumentException(
    121                     "Query on " + uri + " returns null result.");
    122         }
    123 
    124         try {
    125             if ((c.getCount() != 1) || !c.moveToFirst()) {
    126                 throw new IllegalArgumentException(
    127                         "Query on " + uri + " returns 0 or multiple rows.");
    128             }
    129 
    130             String filePath;
    131             if (ImageModel.isMmsUri(uri)) {
    132                 filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME));
    133                 if (TextUtils.isEmpty(filePath)) {
    134                     filePath = c.getString(
    135                             c.getColumnIndexOrThrow(Part._DATA));
    136                 }
    137                 mContentType = c.getString(
    138                         c.getColumnIndexOrThrow(Part.CONTENT_TYPE));
    139             } else {
    140                 filePath = uri.getPath();
    141                 try {
    142                     mContentType = c.getString(
    143                             c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); // mime_type
    144                 } catch (IllegalArgumentException e) {
    145                     mContentType = c.getString(c.getColumnIndexOrThrow("mimetype"));
    146                 }
    147 
    148                 // use the original filename if possible
    149                 int nameIndex = c.getColumnIndex(Images.Media.DISPLAY_NAME);
    150                 if (nameIndex != -1) {
    151                     mSrc = c.getString(nameIndex);
    152                     if (!TextUtils.isEmpty(mSrc)) {
    153                         // Some MMSCs appear to have problems with filenames
    154                         // containing a space.  So just replace them with
    155                         // underscores in the name, which is typically not
    156                         // visible to the user anyway.
    157                         mSrc = mSrc.replace(' ', '_');
    158                     } else {
    159                         mSrc = null;
    160                     }
    161                 }
    162             }
    163             mPath = filePath;
    164             if (mSrc == null) {
    165                 buildSrcFromPath();
    166             }
    167         } catch (IllegalArgumentException e) {
    168             Log.e(TAG, "initFromContentUri couldn't load image uri: " + uri, e);
    169         } finally {
    170             c.close();
    171         }
    172     }
    173 
    174     private void decodeBoundsInfo() {
    175         InputStream input = null;
    176         try {
    177             input = mContext.getContentResolver().openInputStream(mUri);
    178             BitmapFactory.Options opt = new BitmapFactory.Options();
    179             opt.inJustDecodeBounds = true;
    180             BitmapFactory.decodeStream(input, null, opt);
    181             mWidth = opt.outWidth;
    182             mHeight = opt.outHeight;
    183         } catch (FileNotFoundException e) {
    184             // Ignore
    185             Log.e(TAG, "IOException caught while opening stream", e);
    186         } finally {
    187             if (null != input) {
    188                 try {
    189                     input.close();
    190                 } catch (IOException e) {
    191                     // Ignore
    192                     Log.e(TAG, "IOException caught while closing stream", e);
    193                 }
    194             }
    195         }
    196     }
    197 
    198     public String getContentType() {
    199         return mContentType;
    200     }
    201 
    202     public String getSrc() {
    203         return mSrc;
    204     }
    205 
    206     public String getPath() {
    207         return mPath;
    208     }
    209 
    210     public int getWidth() {
    211         return mWidth;
    212     }
    213 
    214     public int getHeight() {
    215         return mHeight;
    216     }
    217 
    218     /**
    219      * Get a version of this image resized to fit the given dimension and byte-size limits. Note
    220      * that the content type of the resulting PduPart may not be the same as the content type of
    221      * this UriImage; always call {@link PduPart#getContentType()} to get the new content type.
    222      *
    223      * @param widthLimit The width limit, in pixels
    224      * @param heightLimit The height limit, in pixels
    225      * @param byteLimit The binary size limit, in bytes
    226      * @return A new PduPart containing the resized image data
    227      */
    228     public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) {
    229         PduPart part = new PduPart();
    230 
    231         byte[] data =  getResizedImageData(mWidth, mHeight,
    232                 widthLimit, heightLimit, byteLimit, mUri, mContext);
    233         if (data == null) {
    234             if (LOCAL_LOGV) {
    235                 Log.v(TAG, "Resize image failed.");
    236             }
    237             return null;
    238         }
    239 
    240         part.setData(data);
    241         // getResizedImageData ALWAYS compresses to JPEG, regardless of the original content type
    242         part.setContentType(ContentType.IMAGE_JPEG.getBytes());
    243 
    244         return part;
    245     }
    246 
    247     private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4;
    248 
    249     /**
    250      * Resize and recompress the image such that it fits the given limits. The resulting byte
    251      * array contains an image in JPEG format, regardless of the original image's content type.
    252      * @param widthLimit The width limit, in pixels
    253      * @param heightLimit The height limit, in pixels
    254      * @param byteLimit The binary size limit, in bytes
    255      * @return A resized/recompressed version of this image, in JPEG format
    256      */
    257     public static byte[] getResizedImageData(int width, int height,
    258             int widthLimit, int heightLimit, int byteLimit, Uri uri, Context context) {
    259         int outWidth = width;
    260         int outHeight = height;
    261 
    262         float scaleFactor = 1.F;
    263         while ((outWidth * scaleFactor > widthLimit) || (outHeight * scaleFactor > heightLimit)) {
    264             scaleFactor *= .75F;
    265         }
    266 
    267         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    268             Log.v(TAG, "getResizedBitmap: wlimit=" + widthLimit +
    269                     ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit +
    270                     ", width=" + width + ", height=" + height +
    271                     ", initialScaleFactor=" + scaleFactor +
    272                     ", uri=" + uri);
    273         }
    274 
    275         InputStream input = null;
    276         try {
    277             ByteArrayOutputStream os = null;
    278             int attempts = 1;
    279             int sampleSize = 1;
    280             BitmapFactory.Options options = new BitmapFactory.Options();
    281             int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY;
    282             Bitmap b = null;
    283 
    284             // In this loop, attempt to decode the stream with the best possible subsampling (we
    285             // start with 1, which means no subsampling - get the original content) without running
    286             // out of memory.
    287             do {
    288                 input = context.getContentResolver().openInputStream(uri);
    289                 options.inSampleSize = sampleSize;
    290                 try {
    291                     b = BitmapFactory.decodeStream(input, null, options);
    292                     if (b == null) {
    293                         return null;    // Couldn't decode and it wasn't because of an exception,
    294                                         // bail.
    295                     }
    296                 } catch (OutOfMemoryError e) {
    297                     Log.w(TAG, "getResizedBitmap: img too large to decode (OutOfMemoryError), " +
    298                             "may try with larger sampleSize. Curr sampleSize=" + sampleSize);
    299                     sampleSize *= 2;    // works best as a power of two
    300                     attempts++;
    301                     continue;
    302                 } finally {
    303                     if (input != null) {
    304                         try {
    305                             input.close();
    306                         } catch (IOException e) {
    307                             Log.e(TAG, e.getMessage(), e);
    308                         }
    309                     }
    310                 }
    311             } while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
    312 
    313             if (b == null) {
    314                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)
    315                         && attempts >= NUMBER_OF_RESIZE_ATTEMPTS) {
    316                     Log.v(TAG, "getResizedImageData: gave up after too many attempts to resize");
    317                 }
    318                 return null;
    319             }
    320 
    321             boolean resultTooBig = true;
    322             attempts = 1;   // reset count for second loop
    323             // In this loop, we attempt to compress/resize the content to fit the given dimension
    324             // and file-size limits.
    325             do {
    326                 try {
    327                     if (options.outWidth > widthLimit || options.outHeight > heightLimit ||
    328                             (os != null && os.size() > byteLimit)) {
    329                         // The decoder does not support the inSampleSize option.
    330                         // Scale the bitmap using Bitmap library.
    331                         int scaledWidth = (int)(outWidth * scaleFactor);
    332                         int scaledHeight = (int)(outHeight * scaleFactor);
    333 
    334                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    335                             Log.v(TAG, "getResizedImageData: retry scaling using " +
    336                                     "Bitmap.createScaledBitmap: w=" + scaledWidth +
    337                                     ", h=" + scaledHeight);
    338                         }
    339 
    340                         b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false);
    341                         if (b == null) {
    342                             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    343                                 Log.v(TAG, "Bitmap.createScaledBitmap returned NULL!");
    344                             }
    345                             return null;
    346                         }
    347                     }
    348 
    349                     // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
    350                     // In case that the image byte size is still too large reduce the quality in
    351                     // proportion to the desired byte size.
    352                     os = new ByteArrayOutputStream();
    353                     b.compress(CompressFormat.JPEG, quality, os);
    354                     int jpgFileSize = os.size();
    355                     if (jpgFileSize > byteLimit) {
    356                         quality = (quality * byteLimit) / jpgFileSize;  // watch for int division!
    357                         if (quality < MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
    358                             quality = MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY;
    359                         }
    360 
    361                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    362                             Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
    363                         }
    364 
    365                         os = new ByteArrayOutputStream();
    366                         b.compress(CompressFormat.JPEG, quality, os);
    367                     }
    368                 } catch (java.lang.OutOfMemoryError e) {
    369                     Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try "
    370                             + " with smaller scale factor, cur scale factor: " + scaleFactor);
    371                     // fall through and keep trying with a smaller scale factor.
    372                 }
    373                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    374                     Log.v(TAG, "attempt=" + attempts
    375                             + " size=" + (os == null ? 0 : os.size())
    376                             + " width=" + outWidth * scaleFactor
    377                             + " height=" + outHeight * scaleFactor
    378                             + " scaleFactor=" + scaleFactor
    379                             + " quality=" + quality);
    380                 }
    381                 scaleFactor *= .75F;
    382                 attempts++;
    383                 resultTooBig = os == null || os.size() > byteLimit;
    384             } while (resultTooBig && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
    385             b.recycle();        // done with the bitmap, release the memory
    386             if (Log.isLoggable(LogTag.APP, Log.VERBOSE) && resultTooBig) {
    387                 Log.v(TAG, "getResizedImageData returning NULL because the result is too big: " +
    388                         " requested max: " + byteLimit + " actual: " + os.size());
    389             }
    390 
    391             return resultTooBig ? null : os.toByteArray();
    392         } catch (FileNotFoundException e) {
    393             Log.e(TAG, e.getMessage(), e);
    394             return null;
    395         } catch (java.lang.OutOfMemoryError e) {
    396             Log.e(TAG, e.getMessage(), e);
    397             return null;
    398         }
    399     }
    400 }
    401