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