/*
* 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.moez.QKSMS.common.google;
import android.content.ContentResolver;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SqliteWrapper;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
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.Log;
import android.webkit.MimeTypeMap;
import com.google.android.mms.ContentType;
import com.google.android.mms.pdu_alt.PduPart;
import com.moez.QKSMS.LogTag;
import com.moez.QKSMS.exif.ExifInterface;
import com.moez.QKSMS.model.ImageModel;
import com.moez.QKSMS.transaction.SmsHelper;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class UriImage {
private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4;
private static final String TAG = "Mms/image";
private static final boolean DEBUG = false;
private static final boolean LOCAL_LOGV = false;
private static final int MMS_PART_ID = 12;
private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURLMatcher.addURI("mms", "part/#", MMS_PART_ID);
}
private final Context mContext;
private final Uri mUri;
private String mContentType;
private String mPath;
private String mSrc;
private int mWidth;
private int mHeight;
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);
}
mContext = context;
mUri = uri;
decodeBoundsInfo();
if (LOCAL_LOGV) {
Log.v(TAG, "UriImage uri: " + uri + " mPath: " + mPath + " mWidth: " + mWidth +
" mHeight: " + mHeight);
}
}
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.
buildSrcFromPath();
}
private void buildSrcFromPath() {
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(' ', '_');
}
private void initFromContentUri(Context context, Uri uri) {
ContentResolver resolver = context.getContentResolver();
Cursor c = SqliteWrapper.query(context, resolver,
uri, null, null, null, null);
mSrc = 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 = uri.getPath();
try {
mContentType = c.getString(
c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); // mime_type
} catch (IllegalArgumentException e) {
try {
mContentType = c.getString(c.getColumnIndexOrThrow("mimetype"));
} catch (IllegalArgumentException ex) {
mContentType = resolver.getType(uri);
Log.v(TAG, "initFromContentUri: " + uri + ", getType => " + mContentType);
}
}
// use the original filename if possible
int nameIndex = c.getColumnIndex(Images.Media.DISPLAY_NAME);
if (nameIndex != -1) {
mSrc = c.getString(nameIndex);
if (!TextUtils.isEmpty(mSrc)) {
// 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(' ', '_');
} else {
mSrc = null;
}
}
}
mPath = filePath;
if (mSrc == null) {
buildSrcFromPath();
}
} catch (IllegalArgumentException e) {
Log.e(TAG, "initFromContentUri couldn't load image uri: " + uri, e);
} 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 String getPath() {
return mPath;
}
public int getWidth() {
return mWidth;
}
public int getHeight() {
return mHeight;
}
/**
* Get a version of this image resized to fit the given dimension and byte-size limits. Note
* that the content type of the resulting PduPart may not be the same as the content type of
* this UriImage; always call @link PduPart#getContentType() to get the new content type.
*
* @param widthLimit The width limit, in pixels
* @param heightLimit The height limit, in pixels
* @param byteLimit The binary size limit, in bytes
* @return A new PduPart containing the resized image data
*/
public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) {
PduPart part = new PduPart();
byte[] data = getResizedImageData(mWidth, mHeight,
widthLimit, heightLimit, byteLimit, mUri, mContext);
if (data == null) {
if (LOCAL_LOGV) {
Log.v(TAG, "Resize image failed.");
}
return null;
}
part.setData(data);
// getResizedImageData ALWAYS compresses to JPEG, regardless of the original content type
part.setContentType(ContentType.IMAGE_JPEG.getBytes());
return part;
}
/**
* Resize and recompress the image such that it fits the given limits. The resulting byte
* array contains an image in JPEG format, regardless of the original image's content type.
* @param widthLimit The width limit, in pixels
* @param heightLimit The height limit, in pixels
* @param byteLimit The binary size limit, in bytes
* @return A resized/recompressed version of this image, in JPEG format
*/
public static byte[] getResizedImageData(int width, int height,
int widthLimit, int heightLimit, int byteLimit, Uri uri, Context context) {
int outWidth = width;
int outHeight = height;
float scaleFactor = 1.F;
while ((outWidth * scaleFactor > widthLimit) || (outHeight * scaleFactor > heightLimit)) {
scaleFactor *= .75F;
}
int orientation = getOrientation(context, uri);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "getResizedBitmap: wlimit=" + widthLimit +
", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit +
", width=" + width + ", height=" + height +
", initialScaleFactor=" + scaleFactor +
", uri=" + uri +
", orientation=" + orientation);
}
InputStream input = null;
ByteArrayOutputStream os = null;
try {
int attempts = 1;
int sampleSize = 1;
BitmapFactory.Options options = new BitmapFactory.Options();
int quality = SmsHelper.IMAGE_COMPRESSION_QUALITY;
Bitmap b = null;
// In this loop, attempt to decode the stream with the best possible subsampling (we
// start with 1, which means no subsampling - get the original content) without running
// out of memory.
do {
input = context.getContentResolver().openInputStream(uri);
options.inSampleSize = sampleSize;
try {
b = BitmapFactory.decodeStream(input, null, options);
if (b == null) {
return null; // Couldn't decode and it wasn't because of an exception,
// bail.
}
} catch (OutOfMemoryError e) {
Log.w(TAG, "getResizedBitmap: img too large to decode (OutOfMemoryError), " +
"may try with larger sampleSize. Curr sampleSize=" + sampleSize);
sampleSize *= 2; // works best as a power of two
attempts++;
continue;
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
} while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
if (b == null) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)
&& attempts >= NUMBER_OF_RESIZE_ATTEMPTS) {
Log.v(TAG, "getResizedImageData: gave up after too many attempts to resize");
}
return null;
}
boolean resultTooBig = true;
attempts = 1; // reset count for second loop
// In this loop, we attempt to compress/resize the content to fit the given dimension
// and file-size limits.
do {
try {
if (options.outWidth > widthLimit || options.outHeight > heightLimit ||
(os != null && os.size() > byteLimit)) {
// The decoder does not support the inSampleSize option.
// Scale the bitmap using Bitmap library.
int scaledWidth = (int)(outWidth * scaleFactor);
int scaledHeight = (int)(outHeight * scaleFactor);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "getResizedImageData: retry scaling using " +
"Bitmap.createScaledBitmap: w=" + scaledWidth +
", h=" + scaledHeight);
}
b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false);
if (b == null) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "Bitmap.createScaledBitmap returned 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.
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
os = new ByteArrayOutputStream();
b.compress(CompressFormat.JPEG, quality, os);
int jpgFileSize = os.size();
if (jpgFileSize > byteLimit) {
quality = (quality * byteLimit) / jpgFileSize; // watch for int division!
if (quality < SmsHelper.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
quality = SmsHelper.MINIMUM_IMAGE_COMPRESSION_QUALITY;
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
os = new ByteArrayOutputStream();
b.compress(CompressFormat.JPEG, quality, os);
}
} catch (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 *= .75F;
attempts++;
resultTooBig = os == null || os.size() > byteLimit;
} while (resultTooBig && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
if (!resultTooBig && orientation != 0) {
// Rotate the final bitmap if we need to.
try {
b = UriImage.rotateBitmap(b, orientation);
os = new ByteArrayOutputStream();
b.compress(CompressFormat.JPEG, quality, os);
resultTooBig = os == null || os.size() > byteLimit;
} catch (OutOfMemoryError e) {
Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError)");
if (os == null) {
return null;
}
}
}
b.recycle(); // done with the bitmap, release the memory
if (Log.isLoggable(LogTag.APP, Log.VERBOSE) && resultTooBig) {
Log.v(TAG, "getResizedImageData returning NULL because the result is too big: " +
" requested max: " + byteLimit + " actual: " + os.size());
}
return resultTooBig ? null : os.toByteArray();
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage(), e);
return null;
} catch (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);
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
}
/**
* Bitmap rotation method
*
* @param bitmap The input bitmap
* @param degrees The rotation angle
*/
public static Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
if (degrees != 0 && bitmap != null) {
final Matrix m = new Matrix();
final int w = bitmap.getWidth();
final int h = bitmap.getHeight();
m.setRotate(degrees, (float) w / 2, (float) h / 2);
try {
final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, m, true);
if (bitmap != rotatedBitmap && rotatedBitmap != null) {
bitmap.recycle();
bitmap = rotatedBitmap;
}
} catch (OutOfMemoryError ex) {
Log.e(TAG, "OOM in rotateBitmap", ex);
// We have no memory to rotate. Return the original bitmap.
}
}
return bitmap;
}
/**
* Returns the number of degrees to rotate the picture, based on the orientation tag in
* the exif data or the orientation column in the database. If there's no tag or column,
* 0 degrees is returned.
*
* @param context Used to get the ContentResolver
* @param uri Path to the image
*/
public static int getOrientation(Context context, Uri uri) {
long dur = System.currentTimeMillis();
if (ContentResolver.SCHEME_FILE.equals(uri.getScheme()) ||
sURLMatcher.match(uri) == MMS_PART_ID) {
// If the uri is a file or an mms part, we have to look at the exif data in the
// file for the orientation because there is no column in the db for the orientation.
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
ExifInterface exif = new ExifInterface();
try {
exif.readExif(inputStream);
Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
if (val == null){
return 0;
}
int orientation =
ExifInterface.getRotationForOrientationValue(val.shortValue());
return orientation;
} catch (IOException e) {
Log.w(TAG, "Failed to read EXIF orientation", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
}
}
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Can't open uri: " + uri, e);
} finally {
dur = System.currentTimeMillis() - dur;
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "UriImage.getOrientation (exif path) took: " + dur + " ms");
}
}
} else {
// Try to get the orientation from the ORIENTATION column in the database. This is much
// faster than reading all the exif tags from the file.
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri,
new String[] {
Images.ImageColumns.ORIENTATION
},
null, null, null);
if (cursor.moveToNext()) {
int ori = cursor.getInt(0);
return ori;
}
} catch (SQLiteException e) {
} catch (IllegalArgumentException e) {
} finally {
if (cursor != null) {
cursor.close();
}
dur = System.currentTimeMillis() - dur;
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "UriImage.getOrientation (db column path) took: " + dur + " ms");
}
}
}
return 0;
}
}