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 android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.UriMatcher;
     23 import android.database.Cursor;
     24 import android.database.sqlite.SQLiteException;
     25 import android.database.sqlite.SqliteWrapper;
     26 import android.graphics.Bitmap;
     27 import android.graphics.Bitmap.CompressFormat;
     28 import android.graphics.BitmapFactory;
     29 import android.graphics.Matrix;
     30 import android.net.Uri;
     31 import android.provider.MediaStore;
     32 import android.provider.MediaStore.Images;
     33 import android.provider.Telephony.Mms.Part;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 import android.webkit.MimeTypeMap;
     37 
     38 import com.android.mms.LogTag;
     39 import com.android.mms.exif.ExifInterface;
     40 import com.android.mms.model.ImageModel;
     41 import com.google.android.mms.ContentType;
     42 import com.google.android.mms.pdu.PduPart;
     43 
     44 import java.io.ByteArrayOutputStream;
     45 import java.io.FileNotFoundException;
     46 import java.io.IOException;
     47 import java.io.InputStream;
     48 
     49 public class UriImage {
     50     private static final String TAG = "Mms/image";
     51     private static final boolean DEBUG = false;
     52     private static final boolean LOCAL_LOGV = false;
     53     private static final int MMS_PART_ID = 12;
     54     private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     55     static {
     56         sURLMatcher.addURI("mms", "part/#", MMS_PART_ID);
     57     }
     58 
     59     private final Context mContext;
     60     private final Uri mUri;
     61     private String mContentType;
     62     private String mPath;
     63     private String mSrc;
     64     private int mWidth;
     65     private int mHeight;
     66 
     67     public UriImage(Context context, Uri uri) {
     68         if ((null == context) || (null == uri)) {
     69             throw new IllegalArgumentException();
     70         }
     71 
     72         String scheme = uri.getScheme();
     73         if (scheme.equals("content")) {
     74             initFromContentUri(context, uri);
     75         } else if (uri.getScheme().equals("file")) {
     76             initFromFile(context, uri);
     77         }
     78 
     79         mContext = context;
     80         mUri = uri;
     81 
     82         decodeBoundsInfo();
     83 
     84         if (LOCAL_LOGV) {
     85             Log.v(TAG, "UriImage uri: " + uri + " mPath: " + mPath + " mWidth: " + mWidth +
     86                     " mHeight: " + mHeight);
     87         }
     88     }
     89 
     90     private void initFromFile(Context context, Uri uri) {
     91         mPath = uri.getPath();
     92         MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
     93         String extension = MimeTypeMap.getFileExtensionFromUrl(mPath);
     94         if (TextUtils.isEmpty(extension)) {
     95             // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle
     96             // urlEncoded strings. Let's try one last time at finding the extension.
     97             int dotPos = mPath.lastIndexOf('.');
     98             if (0 <= dotPos) {
     99                 extension = mPath.substring(dotPos + 1);
    100             }
    101         }
    102         mContentType = mimeTypeMap.getMimeTypeFromExtension(extension);
    103         // It's ok if mContentType is null. Eventually we'll show a toast telling the
    104         // user the picture couldn't be attached.
    105 
    106         buildSrcFromPath();
    107     }
    108 
    109     private void buildSrcFromPath() {
    110         mSrc = mPath.substring(mPath.lastIndexOf('/') + 1);
    111 
    112         if(mSrc.startsWith(".") && mSrc.length() > 1) {
    113             mSrc = mSrc.substring(1);
    114         }
    115 
    116         // Some MMSCs appear to have problems with filenames
    117         // containing a space.  So just replace them with
    118         // underscores in the name, which is typically not
    119         // visible to the user anyway.
    120         mSrc = mSrc.replace(' ', '_');
    121     }
    122 
    123     private void initFromContentUri(Context context, Uri uri) {
    124         ContentResolver resolver = context.getContentResolver();
    125         Cursor c = SqliteWrapper.query(context, resolver,
    126                             uri, null, null, null, null);
    127 
    128         mSrc = null;
    129         if (c == null) {
    130             throw new IllegalArgumentException(
    131                     "Query on " + uri + " returns null result.");
    132         }
    133 
    134         try {
    135             if ((c.getCount() != 1) || !c.moveToFirst()) {
    136                 throw new IllegalArgumentException(
    137                         "Query on " + uri + " returns 0 or multiple rows.");
    138             }
    139 
    140             String filePath;
    141             if (ImageModel.isMmsUri(uri)) {
    142                 filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME));
    143                 if (TextUtils.isEmpty(filePath)) {
    144                     filePath = c.getString(
    145                             c.getColumnIndexOrThrow(Part._DATA));
    146                 }
    147                 mContentType = c.getString(
    148                         c.getColumnIndexOrThrow(Part.CONTENT_TYPE));
    149             } else {
    150                 filePath = uri.getPath();
    151                 try {
    152                     mContentType = c.getString(
    153                             c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); // mime_type
    154                 } catch (IllegalArgumentException e) {
    155                     try {
    156                         mContentType = c.getString(c.getColumnIndexOrThrow("mimetype"));
    157                     } catch (IllegalArgumentException ex) {
    158                         mContentType = resolver.getType(uri);
    159                         Log.v(TAG, "initFromContentUri: " + uri + ", getType => " + mContentType);
    160                     }
    161                 }
    162 
    163                 // use the original filename if possible
    164                 int nameIndex = c.getColumnIndex(Images.Media.DISPLAY_NAME);
    165                 if (nameIndex != -1) {
    166                     mSrc = c.getString(nameIndex);
    167                     if (!TextUtils.isEmpty(mSrc)) {
    168                         // Some MMSCs appear to have problems with filenames
    169                         // containing a space.  So just replace them with
    170                         // underscores in the name, which is typically not
    171                         // visible to the user anyway.
    172                         mSrc = mSrc.replace(' ', '_');
    173                     } else {
    174                         mSrc = null;
    175                     }
    176                 }
    177             }
    178             mPath = filePath;
    179             if (mSrc == null) {
    180                 buildSrcFromPath();
    181             }
    182         } catch (IllegalArgumentException e) {
    183             Log.e(TAG, "initFromContentUri couldn't load image uri: " + uri, e);
    184         } finally {
    185             c.close();
    186         }
    187     }
    188 
    189     private void decodeBoundsInfo() {
    190         InputStream input = null;
    191         try {
    192             input = mContext.getContentResolver().openInputStream(mUri);
    193             BitmapFactory.Options opt = new BitmapFactory.Options();
    194             opt.inJustDecodeBounds = true;
    195             BitmapFactory.decodeStream(input, null, opt);
    196             mWidth = opt.outWidth;
    197             mHeight = opt.outHeight;
    198         } catch (FileNotFoundException e) {
    199             // Ignore
    200             Log.e(TAG, "IOException caught while opening stream", e);
    201         } finally {
    202             if (null != input) {
    203                 try {
    204                     input.close();
    205                 } catch (IOException e) {
    206                     // Ignore
    207                     Log.e(TAG, "IOException caught while closing stream", e);
    208                 }
    209             }
    210         }
    211     }
    212 
    213     public String getContentType() {
    214         return mContentType;
    215     }
    216 
    217     public String getSrc() {
    218         return mSrc;
    219     }
    220 
    221     public String getPath() {
    222         return mPath;
    223     }
    224 
    225     public int getWidth() {
    226         return mWidth;
    227     }
    228 
    229     public int getHeight() {
    230         return mHeight;
    231     }
    232 
    233     /**
    234      * Get a version of this image resized to fit the given dimension and byte-size limits. Note
    235      * that the content type of the resulting PduPart may not be the same as the content type of
    236      * this UriImage; always call {@link PduPart#getContentType()} to get the new content type.
    237      *
    238      * @param widthLimit The width limit, in pixels
    239      * @param heightLimit The height limit, in pixels
    240      * @param byteLimit The binary size limit, in bytes
    241      * @return A new PduPart containing the resized image data
    242      */
    243     public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) {
    244         PduPart part = new PduPart();
    245 
    246         byte[] data =  getResizedImageData(mWidth, mHeight,
    247                 widthLimit, heightLimit, byteLimit, mUri, mContext);
    248         if (data == null) {
    249             if (LOCAL_LOGV) {
    250                 Log.v(TAG, "Resize image failed.");
    251             }
    252             return null;
    253         }
    254 
    255         part.setData(data);
    256         // getResizedImageData ALWAYS compresses to JPEG, regardless of the original content type
    257         part.setContentType(ContentType.IMAGE_JPEG.getBytes());
    258 
    259         return part;
    260     }
    261 
    262     private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4;
    263 
    264     /**
    265      * Resize and recompress the image such that it fits the given limits. The resulting byte
    266      * array contains an image in JPEG format, regardless of the original image's content type.
    267      * @param widthLimit The width limit, in pixels
    268      * @param heightLimit The height limit, in pixels
    269      * @param byteLimit The binary size limit, in bytes
    270      * @return A resized/recompressed version of this image, in JPEG format
    271      */
    272     public static byte[] getResizedImageData(int width, int height,
    273             int widthLimit, int heightLimit, int byteLimit, Uri uri, Context context) {
    274         int outWidth = width;
    275         int outHeight = height;
    276 
    277         float scaleFactor = 1.F;
    278         while ((outWidth * scaleFactor > widthLimit) || (outHeight * scaleFactor > heightLimit)) {
    279             scaleFactor *= .75F;
    280         }
    281 
    282         int orientation = getOrientation(context, uri);
    283 
    284         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    285             Log.v(TAG, "getResizedBitmap: wlimit=" + widthLimit +
    286                     ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit +
    287                     ", width=" + width + ", height=" + height +
    288                     ", initialScaleFactor=" + scaleFactor +
    289                     ", uri=" + uri +
    290                     ", orientation=" + orientation);
    291         }
    292 
    293         InputStream input = null;
    294         ByteArrayOutputStream os = null;
    295         try {
    296             int attempts = 1;
    297             int sampleSize = 1;
    298             BitmapFactory.Options options = new BitmapFactory.Options();
    299             int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY;
    300             Bitmap b = null;
    301 
    302             // In this loop, attempt to decode the stream with the best possible subsampling (we
    303             // start with 1, which means no subsampling - get the original content) without running
    304             // out of memory.
    305             do {
    306                 input = context.getContentResolver().openInputStream(uri);
    307                 options.inSampleSize = sampleSize;
    308                 try {
    309                     b = BitmapFactory.decodeStream(input, null, options);
    310                     if (b == null) {
    311                         return null;    // Couldn't decode and it wasn't because of an exception,
    312                                         // bail.
    313                     }
    314                 } catch (OutOfMemoryError e) {
    315                     Log.w(TAG, "getResizedBitmap: img too large to decode (OutOfMemoryError), " +
    316                             "may try with larger sampleSize. Curr sampleSize=" + sampleSize);
    317                     sampleSize *= 2;    // works best as a power of two
    318                     attempts++;
    319                     continue;
    320                 } finally {
    321                     if (input != null) {
    322                         try {
    323                             input.close();
    324                         } catch (IOException e) {
    325                             Log.e(TAG, e.getMessage(), e);
    326                         }
    327                     }
    328                 }
    329             } while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
    330 
    331             if (b == null) {
    332                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)
    333                         && attempts >= NUMBER_OF_RESIZE_ATTEMPTS) {
    334                     Log.v(TAG, "getResizedImageData: gave up after too many attempts to resize");
    335                 }
    336                 return null;
    337             }
    338 
    339             boolean resultTooBig = true;
    340             attempts = 1;   // reset count for second loop
    341             // In this loop, we attempt to compress/resize the content to fit the given dimension
    342             // and file-size limits.
    343             do {
    344                 try {
    345                     if (options.outWidth > widthLimit || options.outHeight > heightLimit ||
    346                             (os != null && os.size() > byteLimit)) {
    347                         // The decoder does not support the inSampleSize option.
    348                         // Scale the bitmap using Bitmap library.
    349                         int scaledWidth = (int)(outWidth * scaleFactor);
    350                         int scaledHeight = (int)(outHeight * scaleFactor);
    351 
    352                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    353                             Log.v(TAG, "getResizedImageData: retry scaling using " +
    354                                     "Bitmap.createScaledBitmap: w=" + scaledWidth +
    355                                     ", h=" + scaledHeight);
    356                         }
    357 
    358                         b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false);
    359                         if (b == null) {
    360                             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    361                                 Log.v(TAG, "Bitmap.createScaledBitmap returned NULL!");
    362                             }
    363                             return null;
    364                         }
    365                     }
    366 
    367                     // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
    368                     // In case that the image byte size is still too large reduce the quality in
    369                     // proportion to the desired byte size.
    370                     if (os != null) {
    371                         try {
    372                             os.close();
    373                         } catch (IOException e) {
    374                             Log.e(TAG, e.getMessage(), e);
    375                         }
    376                     }
    377                     os = new ByteArrayOutputStream();
    378                     b.compress(CompressFormat.JPEG, quality, os);
    379                     int jpgFileSize = os.size();
    380                     if (jpgFileSize > byteLimit) {
    381                         quality = (quality * byteLimit) / jpgFileSize;  // watch for int division!
    382                         if (quality < MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
    383                             quality = MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY;
    384                         }
    385 
    386                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    387                             Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
    388                         }
    389 
    390                         if (os != null) {
    391                             try {
    392                                 os.close();
    393                             } catch (IOException e) {
    394                                 Log.e(TAG, e.getMessage(), e);
    395                             }
    396                         }
    397                         os = new ByteArrayOutputStream();
    398                         b.compress(CompressFormat.JPEG, quality, os);
    399                     }
    400                 } catch (java.lang.OutOfMemoryError e) {
    401                     Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try "
    402                             + " with smaller scale factor, cur scale factor: " + scaleFactor);
    403                     // fall through and keep trying with a smaller scale factor.
    404                 }
    405                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    406                     Log.v(TAG, "attempt=" + attempts
    407                             + " size=" + (os == null ? 0 : os.size())
    408                             + " width=" + outWidth * scaleFactor
    409                             + " height=" + outHeight * scaleFactor
    410                             + " scaleFactor=" + scaleFactor
    411                             + " quality=" + quality);
    412                 }
    413                 scaleFactor *= .75F;
    414                 attempts++;
    415                 resultTooBig = os == null || os.size() > byteLimit;
    416             } while (resultTooBig && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
    417             if (!resultTooBig && orientation != 0) {
    418                 // Rotate the final bitmap if we need to.
    419                 try {
    420                     b = UriImage.rotateBitmap(b, orientation);
    421                     os = new ByteArrayOutputStream();
    422                     b.compress(CompressFormat.JPEG, quality, os);
    423                     resultTooBig = os == null || os.size() > byteLimit;
    424                 } catch (java.lang.OutOfMemoryError e) {
    425                     Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError)");
    426                     if (os == null) {
    427                         return null;
    428                     }
    429                 }
    430             }
    431 
    432             b.recycle();        // done with the bitmap, release the memory
    433             if (Log.isLoggable(LogTag.APP, Log.VERBOSE) && resultTooBig) {
    434                 Log.v(TAG, "getResizedImageData returning NULL because the result is too big: " +
    435                         " requested max: " + byteLimit + " actual: " + os.size());
    436             }
    437 
    438             return resultTooBig ? null : os.toByteArray();
    439         } catch (FileNotFoundException e) {
    440             Log.e(TAG, e.getMessage(), e);
    441             return null;
    442         } catch (java.lang.OutOfMemoryError e) {
    443             Log.e(TAG, e.getMessage(), e);
    444             return null;
    445         } finally {
    446             if (input != null) {
    447                 try {
    448                     input.close();
    449                 } catch (IOException e) {
    450                     Log.e(TAG, e.getMessage(), e);
    451                 }
    452             }
    453             if (os != null) {
    454                 try {
    455                     os.close();
    456                 } catch (IOException e) {
    457                     Log.e(TAG, e.getMessage(), e);
    458                 }
    459             }
    460         }
    461     }
    462 
    463     /**
    464      * Bitmap rotation method
    465      *
    466      * @param bitmap The input bitmap
    467      * @param degrees The rotation angle
    468      */
    469     public static Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
    470         if (degrees != 0 && bitmap != null) {
    471             final Matrix m = new Matrix();
    472             final int w = bitmap.getWidth();
    473             final int h = bitmap.getHeight();
    474             m.setRotate(degrees, (float) w / 2, (float) h / 2);
    475 
    476             try {
    477                 final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, m, true);
    478                 if (bitmap != rotatedBitmap && rotatedBitmap != null) {
    479                     bitmap.recycle();
    480                     bitmap = rotatedBitmap;
    481                 }
    482             } catch (OutOfMemoryError ex) {
    483                 Log.e(TAG, "OOM in rotateBitmap", ex);
    484                 // We have no memory to rotate. Return the original bitmap.
    485             }
    486         }
    487 
    488         return bitmap;
    489     }
    490 
    491     /**
    492      * Returns the number of degrees to rotate the picture, based on the orientation tag in
    493      * the exif data or the orientation column in the database. If there's no tag or column,
    494      * 0 degrees is returned.
    495      *
    496      * @param context Used to get the ContentResolver
    497      * @param uri Path to the image
    498      */
    499     public static int getOrientation(Context context, Uri uri) {
    500         long dur = System.currentTimeMillis();
    501         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme()) ||
    502                 sURLMatcher.match(uri) == MMS_PART_ID) {
    503             // If the uri is a file or an mms part, we have to look at the exif data in the
    504             // file for the orientation because there is no column in the db for the orientation.
    505             try {
    506                 InputStream inputStream = context.getContentResolver().openInputStream(uri);
    507                 ExifInterface exif = new ExifInterface();
    508                 try {
    509                     exif.readExif(inputStream);
    510                     Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
    511                     if (val == null){
    512                         return 0;
    513                     }
    514                     int orientation =
    515                             ExifInterface.getRotationForOrientationValue(val.shortValue());
    516                     return orientation;
    517                 } catch (IOException e) {
    518                     Log.w(TAG, "Failed to read EXIF orientation", e);
    519                 } finally {
    520                     if (inputStream != null) {
    521                         try {
    522                             inputStream.close();
    523                         } catch (IOException e) {
    524                         }
    525                     }
    526                 }
    527             } catch (FileNotFoundException e) {
    528                 Log.e(TAG, "Can't open uri: " + uri, e);
    529             } finally {
    530                 dur = System.currentTimeMillis() - dur;
    531                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    532                     Log.v(TAG, "UriImage.getOrientation (exif path) took: " + dur + " ms");
    533                 }
    534             }
    535         } else {
    536             // Try to get the orientation from the ORIENTATION column in the database. This is much
    537             // faster than reading all the exif tags from the file.
    538             Cursor cursor = null;
    539             try {
    540                 cursor = context.getContentResolver().query(uri,
    541                         new String[] {
    542                             MediaStore.Images.ImageColumns.ORIENTATION
    543                         },
    544                         null, null, null);
    545                 if (cursor.moveToNext()) {
    546                     int ori = cursor.getInt(0);
    547                     return ori;
    548                 }
    549             } catch (SQLiteException e) {
    550             } catch (IllegalArgumentException e) {
    551             } finally {
    552                 if (cursor != null) {
    553                     cursor.close();
    554                 }
    555                 dur = System.currentTimeMillis() - dur;
    556                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
    557                     Log.v(TAG, "UriImage.getOrientation (db column path) took: " + dur + " ms");
    558                 }
    559             }
    560         }
    561         return 0;
    562     }
    563 }
    564