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 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 mSrc = mPath.substring(mPath.lastIndexOf('/') + 1); 70 71 if(mSrc.startsWith(".") && mSrc.length() > 1) { 72 mSrc = mSrc.substring(1); 73 } 74 75 // Some MMSCs appear to have problems with filenames 76 // containing a space. So just replace them with 77 // underscores in the name, which is typically not 78 // visible to the user anyway. 79 mSrc = mSrc.replace(' ', '_'); 80 81 mContext = context; 82 mUri = uri; 83 84 decodeBoundsInfo(); 85 86 if (LOCAL_LOGV) { 87 Log.v(TAG, "UriImage uri: " + uri + " mPath: " + mPath + " mWidth: " + mWidth + 88 " mHeight: " + mHeight); 89 } 90 } 91 initFromFile(Context context, Uri uri)92 private void initFromFile(Context context, Uri uri) { 93 mPath = uri.getPath(); 94 MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); 95 String extension = MimeTypeMap.getFileExtensionFromUrl(mPath); 96 if (TextUtils.isEmpty(extension)) { 97 // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle 98 // urlEncoded strings. Let's try one last time at finding the extension. 99 int dotPos = mPath.lastIndexOf('.'); 100 if (0 <= dotPos) { 101 extension = mPath.substring(dotPos + 1); 102 } 103 } 104 mContentType = mimeTypeMap.getMimeTypeFromExtension(extension); 105 // It's ok if mContentType is null. Eventually we'll show a toast telling the 106 // user the picture couldn't be attached. 107 } 108 initFromContentUri(Context context, Uri uri)109 private void initFromContentUri(Context context, Uri uri) { 110 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 111 uri, null, null, null, null); 112 113 if (c == null) { 114 throw new IllegalArgumentException( 115 "Query on " + uri + " returns null result."); 116 } 117 118 try { 119 if ((c.getCount() != 1) || !c.moveToFirst()) { 120 throw new IllegalArgumentException( 121 "Query on " + uri + " returns 0 or multiple rows."); 122 } 123 124 String filePath; 125 if (ImageModel.isMmsUri(uri)) { 126 filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME)); 127 if (TextUtils.isEmpty(filePath)) { 128 filePath = c.getString( 129 c.getColumnIndexOrThrow(Part._DATA)); 130 } 131 mContentType = c.getString( 132 c.getColumnIndexOrThrow(Part.CONTENT_TYPE)); 133 } else { 134 filePath = uri.getPath(); 135 mContentType = c.getString( 136 c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); 137 } 138 mPath = filePath; 139 } finally { 140 c.close(); 141 } 142 } 143 decodeBoundsInfo()144 private void decodeBoundsInfo() { 145 InputStream input = null; 146 try { 147 input = mContext.getContentResolver().openInputStream(mUri); 148 BitmapFactory.Options opt = new BitmapFactory.Options(); 149 opt.inJustDecodeBounds = true; 150 BitmapFactory.decodeStream(input, null, opt); 151 mWidth = opt.outWidth; 152 mHeight = opt.outHeight; 153 } catch (FileNotFoundException e) { 154 // Ignore 155 Log.e(TAG, "IOException caught while opening stream", e); 156 } finally { 157 if (null != input) { 158 try { 159 input.close(); 160 } catch (IOException e) { 161 // Ignore 162 Log.e(TAG, "IOException caught while closing stream", e); 163 } 164 } 165 } 166 } 167 getContentType()168 public String getContentType() { 169 return mContentType; 170 } 171 getSrc()172 public String getSrc() { 173 return mSrc; 174 } 175 getWidth()176 public int getWidth() { 177 return mWidth; 178 } 179 getHeight()180 public int getHeight() { 181 return mHeight; 182 } 183 184 /** 185 * Get a version of this image resized to fit the given dimension and byte-size limits. Note 186 * that the content type of the resulting PduPart may not be the same as the content type of 187 * this UriImage; always call {@link PduPart#getContentType()} to get the new content type. 188 * 189 * @param widthLimit The width limit, in pixels 190 * @param heightLimit The height limit, in pixels 191 * @param byteLimit The binary size limit, in bytes 192 * @return A new PduPart containing the resized image data 193 */ getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit)194 public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) { 195 PduPart part = new PduPart(); 196 197 byte[] data = getResizedImageData(widthLimit, heightLimit, byteLimit); 198 if (data == null) { 199 if (LOCAL_LOGV) { 200 Log.v(TAG, "Resize image failed."); 201 } 202 return null; 203 } 204 205 part.setData(data); 206 // getResizedImageData ALWAYS compresses to JPEG, regardless of the original content type 207 part.setContentType(ContentType.IMAGE_JPEG.getBytes()); 208 209 return part; 210 } 211 212 private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4; 213 214 /** 215 * Resize and recompress the image such that it fits the given limits. The resulting byte 216 * array contains an image in JPEG format, regardless of the original image's content type. 217 * @param widthLimit The width limit, in pixels 218 * @param heightLimit The height limit, in pixels 219 * @param byteLimit The binary size limit, in bytes 220 * @return A resized/recompressed version of this image, in JPEG format 221 */ getResizedImageData(int widthLimit, int heightLimit, int byteLimit)222 private byte[] getResizedImageData(int widthLimit, int heightLimit, int byteLimit) { 223 int outWidth = mWidth; 224 int outHeight = mHeight; 225 226 float scaleFactor = 1.F; 227 while ((outWidth * scaleFactor > widthLimit) || (outHeight * scaleFactor > heightLimit)) { 228 scaleFactor *= .75F; 229 } 230 231 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 232 Log.v(TAG, "getResizedImageData: wlimit=" + widthLimit + 233 ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit + 234 ", mWidth=" + mWidth + ", mHeight=" + mHeight + 235 ", initialScaleFactor=" + scaleFactor + 236 ", mUri=" + mUri); 237 } 238 239 InputStream input = null; 240 try { 241 ByteArrayOutputStream os = null; 242 int attempts = 1; 243 int sampleSize = 1; 244 BitmapFactory.Options options = new BitmapFactory.Options(); 245 int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY; 246 Bitmap b = null; 247 248 // In this loop, attempt to decode the stream with the best possible subsampling (we 249 // start with 1, which means no subsampling - get the original content) without running 250 // out of memory. 251 do { 252 input = mContext.getContentResolver().openInputStream(mUri); 253 options.inSampleSize = sampleSize; 254 try { 255 b = BitmapFactory.decodeStream(input, null, options); 256 } catch (OutOfMemoryError e) { 257 Log.w(TAG, "getResizedImageData: img too large to decode (OutOfMemoryError), " + 258 "may try with larger sampleSize. Curr sampleSize=" + sampleSize); 259 sampleSize *= 2; // works best as a power of two 260 attempts++; 261 continue; 262 } finally { 263 if (input != null) { 264 try { 265 input.close(); 266 } catch (IOException e) { 267 Log.e(TAG, e.getMessage(), e); 268 } 269 } 270 } 271 } while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS); 272 273 if (b == null) { 274 return null; 275 } 276 277 attempts = 1; // reset count for second loop 278 // In this loop, we attempt to compress/resize the content to fit the given dimension 279 // and file-size limits. 280 do { 281 try { 282 if (options.outWidth > widthLimit || options.outHeight > heightLimit || 283 (os != null && os.size() > byteLimit)) { 284 // The decoder does not support the inSampleSize option. 285 // Scale the bitmap using Bitmap library. 286 int scaledWidth = (int)(outWidth * scaleFactor); 287 int scaledHeight = (int)(outHeight * scaleFactor); 288 289 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 290 Log.v(TAG, "getResizedImageData: retry scaling using " + 291 "Bitmap.createScaledBitmap: w=" + scaledWidth + 292 ", h=" + scaledHeight); 293 } 294 295 b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false); 296 if (b == null) { 297 return null; 298 } 299 } 300 301 // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY. 302 // In case that the image byte size is still too large reduce the quality in 303 // proportion to the desired byte size. Should the quality fall below 304 // MINIMUM_IMAGE_COMPRESSION_QUALITY skip a compression attempt and we will enter 305 // the next round with a smaller image to start with. 306 os = new ByteArrayOutputStream(); 307 b.compress(CompressFormat.JPEG, quality, os); 308 int jpgFileSize = os.size(); 309 if (jpgFileSize > byteLimit) { 310 quality = quality * byteLimit / jpgFileSize; 311 if (quality < MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) { 312 quality = MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY; 313 } 314 315 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 316 Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality); 317 } 318 319 os = new ByteArrayOutputStream(); 320 b.compress(CompressFormat.JPEG, quality, os); 321 } 322 } catch (java.lang.OutOfMemoryError e) { 323 Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try " 324 + " with smaller scale factor, cur scale factor: " + scaleFactor); 325 // fall through and keep trying with a smaller scale factor. 326 } 327 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 328 Log.v(TAG, "attempt=" + attempts 329 + " size=" + (os == null ? 0 : os.size()) 330 + " width=" + outWidth * scaleFactor 331 + " height=" + outHeight * scaleFactor 332 + " scaleFactor=" + scaleFactor 333 + " quality=" + quality); 334 } 335 scaleFactor *= .75F; 336 attempts++; 337 } while ((os == null || os.size() > byteLimit) && attempts < NUMBER_OF_RESIZE_ATTEMPTS); 338 b.recycle(); // done with the bitmap, release the memory 339 340 return os == null ? null : os.toByteArray(); 341 } catch (FileNotFoundException e) { 342 Log.e(TAG, e.getMessage(), e); 343 return null; 344 } catch (java.lang.OutOfMemoryError e) { 345 Log.e(TAG, e.getMessage(), e); 346 return null; 347 } 348 } 349 } 350