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