/* * Copyright (C) 2008 Esmertec AG. * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mms.ui; import com.android.mms.model.ImageModel; import com.android.mms.LogTag; import com.google.android.mms.pdu.PduPart; import android.database.sqlite.SqliteWrapper; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Bitmap.CompressFormat; import android.graphics.Matrix; import android.net.Uri; import android.provider.MediaStore.Images; import android.provider.Telephony.Mms.Part; import android.text.TextUtils; import android.util.Config; import android.util.Log; import android.webkit.MimeTypeMap; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; public class UriImage { private static final String TAG = "Mms/image"; private static final boolean DEBUG = true; private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; private final Context mContext; private final Uri mUri; private String mContentType; private String mPath; private String mSrc; private int mWidth; private int mHeight; private int mOrientation = 0; public UriImage(Context context, Uri uri) { if ((null == context) || (null == uri)) { throw new IllegalArgumentException(); } String scheme = uri.getScheme(); if (scheme.equals("content")) { initFromContentUri(context, uri); } else if (uri.getScheme().equals("file")) { initFromFile(context, uri); } mSrc = mPath.substring(mPath.lastIndexOf('/') + 1); if(mSrc.startsWith(".") && mSrc.length() > 1) { mSrc = mSrc.substring(1); } // Some MMSCs appear to have problems with filenames // containing a space. So just replace them with // underscores in the name, which is typically not // visible to the user anyway. mSrc = mSrc.replace(' ', '_'); mSrc = System.currentTimeMillis()+"_"+mSrc; mContext = context; mUri = uri; decodeBoundsInfo(); } private void initFromFile(Context context, Uri uri) { mPath = uri.getPath(); MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); String extension = MimeTypeMap.getFileExtensionFromUrl(mPath); if (TextUtils.isEmpty(extension)) { // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle // urlEncoded strings. Let's try one last time at finding the extension. int dotPos = mPath.lastIndexOf('.'); if (0 <= dotPos) { extension = mPath.substring(dotPos + 1); } } mContentType = mimeTypeMap.getMimeTypeFromExtension(extension); // It's ok if mContentType is null. Eventually we'll show a toast telling the // user the picture couldn't be attached. } private void initFromContentUri(Context context, Uri uri) { Cursor c = SqliteWrapper.query(context, context.getContentResolver(), uri, null, null, null, null); if (c == null) { throw new IllegalArgumentException( "Query on " + uri + " returns null result."); } try { if ((c.getCount() != 1) || !c.moveToFirst()) { throw new IllegalArgumentException( "Query on " + uri + " returns 0 or multiple rows."); } String filePath; if (ImageModel.isMmsUri(uri)) { filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME)); if (TextUtils.isEmpty(filePath)) { filePath = c.getString( c.getColumnIndexOrThrow(Part._DATA)); } mContentType = c.getString( c.getColumnIndexOrThrow(Part.CONTENT_TYPE)); } else { filePath = c.getString(c.getColumnIndexOrThrow(Images.Media.DATA)); mContentType = c.getString(c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); if (mContentType.contains("image")) { mOrientation = c.getInt(c.getColumnIndexOrThrow(Images.Media.ORIENTATION)); } Log.d(TAG, "initFromContentUri image orientation:" + mOrientation); } mPath = filePath; } finally { c.close(); } } private void decodeBoundsInfo() { InputStream input = null; try { input = mContext.getContentResolver().openInputStream(mUri); BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inJustDecodeBounds = true; BitmapFactory.decodeStream(input, null, opt); mWidth = opt.outWidth; mHeight = opt.outHeight; } catch (FileNotFoundException e) { // Ignore Log.e(TAG, "IOException caught while opening stream", e); } finally { if (null != input) { try { input.close(); } catch (IOException e) { // Ignore Log.e(TAG, "IOException caught while closing stream", e); } } } } public String getContentType() { return mContentType; } public String getSrc() { return mSrc; } public int getWidth() { return mWidth; } public int getHeight() { return mHeight; } public int getOrientation() { return mOrientation; } public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) { PduPart part = new PduPart(); byte[] data = getResizedImageData(widthLimit, heightLimit, byteLimit); if (data == null) { if (LOCAL_LOGV) { Log.v(TAG, "Resize image failed."); } return null; } part.setData(data); part.setContentType(getContentType().getBytes()); return part; } private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4; private byte[] getResizedImageData(int widthLimit, int heightLimit, int byteLimit) { int outWidth = mWidth; int outHeight = mHeight; int scaleFactor = 1; while ((outWidth / scaleFactor > widthLimit) || (outHeight / scaleFactor > heightLimit)) { scaleFactor *= 2; } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.v(TAG, "getResizedImageData: wlimit=" + widthLimit + ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit + ", mWidth=" + mWidth + ", mHeight=" + mHeight + ", initialScaleFactor=" + scaleFactor); } InputStream input = null; try { ByteArrayOutputStream os = null; int attempts = 1; do { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = scaleFactor; input = mContext.getContentResolver().openInputStream(mUri); int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY; try { Bitmap b = BitmapFactory.decodeStream(input, null, options); if (b == null) { return null; } if (options.outWidth > widthLimit || options.outHeight > heightLimit || mOrientation != 0) { // The decoder does not support the inSampleSize option. // Scale the bitmap using Bitmap library. int scaledWidth = outWidth / scaleFactor; int scaledHeight = outHeight / scaleFactor; if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.v(TAG, "getResizedImageData: retry scaling using " + "Bitmap.createScaledBitmap: w=" + scaledWidth + ", h=" + scaledHeight); } if (mContentType.contains("image")) { b = rotateAndScale(b, mOrientation, scaledWidth, scaledHeight, false); } else { b = Bitmap.createScaledBitmap(b, outWidth / scaleFactor, outHeight / scaleFactor, false); } if (b == null) { return null; } } // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY. // In case that the image byte size is still too large reduce the quality in // proportion to the desired byte size. Should the quality fall below // MINIMUM_IMAGE_COMPRESSION_QUALITY skip a compression attempt and we will enter // the next round with a smaller image to start with. os = new ByteArrayOutputStream(); b.compress(CompressFormat.JPEG, quality, os); int jpgFileSize = os.size(); if (jpgFileSize > byteLimit) { int reducedQuality = quality * byteLimit / jpgFileSize; if (reducedQuality >= MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) { quality = reducedQuality; if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality); } os = new ByteArrayOutputStream(); b.compress(CompressFormat.JPEG, quality, os); } } b.recycle(); // done with the bitmap, release the memory } catch (java.lang.OutOfMemoryError e) { Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try " + " with smaller scale factor, cur scale factor: " + scaleFactor); // fall through and keep trying with a smaller scale factor. } if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.v(TAG, "attempt=" + attempts + " size=" + (os == null ? 0 : os.size()) + " width=" + outWidth / scaleFactor + " height=" + outHeight / scaleFactor + " scaleFactor=" + scaleFactor + " quality=" + quality); } scaleFactor *= 2; attempts++; } while ((os == null || os.size() > byteLimit) && attempts < NUMBER_OF_RESIZE_ATTEMPTS); return os == null ? null : os.toByteArray(); } catch (FileNotFoundException e) { Log.e(TAG, e.getMessage(), e); return null; } catch (java.lang.OutOfMemoryError e) { Log.e(TAG, e.getMessage(), e); return null; } finally { if (input != null) { try { input.close(); } catch (IOException e) { Log.e(TAG, e.getMessage(), e); } } } } private Bitmap rotateAndScale(Bitmap b, int degrees, int dstWidth, int dstHeight, boolean filter) { if (b != null) { Matrix m = new Matrix(); final int width = b.getWidth(); final int height = b.getHeight(); final float sx = dstWidth / (float) width; final float sy = dstHeight / (float) height; m.setScale(sx, sy); if (degrees != 0) { m.setRotate(degrees, (float) dstWidth / 2, (float) dstHeight / 2); } // Log.d(TAG, "degree="+degrees+";sx="+sx+";sy="+sy); try { Bitmap b2 = Bitmap.createBitmap(b, 0, 0, width, height, m, filter); if (b != b2) { b.recycle(); b = b2; } } catch (OutOfMemoryError ex) { // We have no memory to rotate. Return the original bitmap. } } return b; } }