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