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 import com.google.android.mms.pdu.PduPart; 23 import android.database.sqlite.SqliteWrapper; 24 25 import android.content.Context; 26 import android.database.Cursor; 27 import android.graphics.Bitmap; 28 import android.graphics.BitmapFactory; 29 import android.graphics.Bitmap.CompressFormat; 30 import android.net.Uri; 31 import android.provider.MediaStore.Images; 32 import android.provider.Telephony.Mms.Part; 33 import android.text.TextUtils; 34 import android.util.Config; 35 import android.util.Log; 36 import android.webkit.MimeTypeMap; 37 38 import java.io.ByteArrayOutputStream; 39 import java.io.FileNotFoundException; 40 import java.io.IOException; 41 import java.io.InputStream; 42 43 public class UriImage { 44 private static final String TAG = "Mms/image"; 45 private static final boolean DEBUG = true; 46 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 47 48 private final Context mContext; 49 private final Uri mUri; 50 private String mContentType; 51 private String mPath; 52 private String mSrc; 53 private int mWidth; 54 private int mHeight; 55 56 public UriImage(Context context, Uri uri) { 57 if ((null == context) || (null == uri)) { 58 throw new IllegalArgumentException(); 59 } 60 61 String scheme = uri.getScheme(); 62 if (scheme.equals("content")) { 63 initFromContentUri(context, uri); 64 } else if (uri.getScheme().equals("file")) { 65 initFromFile(context, uri); 66 } 67 68 mSrc = mPath.substring(mPath.lastIndexOf('/') + 1); 69 70 if(mSrc.startsWith(".") && mSrc.length() > 1) { 71 mSrc = mSrc.substring(1); 72 } 73 74 // Some MMSCs appear to have problems with filenames 75 // containing a space. So just replace them with 76 // underscores in the name, which is typically not 77 // visible to the user anyway. 78 mSrc = mSrc.replace(' ', '_'); 79 80 mContext = context; 81 mUri = uri; 82 83 decodeBoundsInfo(); 84 } 85 86 private void initFromFile(Context context, Uri uri) { 87 mPath = uri.getPath(); 88 MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); 89 String extension = MimeTypeMap.getFileExtensionFromUrl(mPath); 90 if (TextUtils.isEmpty(extension)) { 91 // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle 92 // urlEncoded strings. Let's try one last time at finding the extension. 93 int dotPos = mPath.lastIndexOf('.'); 94 if (0 <= dotPos) { 95 extension = mPath.substring(dotPos + 1); 96 } 97 } 98 mContentType = mimeTypeMap.getMimeTypeFromExtension(extension); 99 // It's ok if mContentType is null. Eventually we'll show a toast telling the 100 // user the picture couldn't be attached. 101 } 102 103 private void initFromContentUri(Context context, Uri uri) { 104 Cursor c = SqliteWrapper.query(context, context.getContentResolver(), 105 uri, null, null, null, null); 106 107 if (c == null) { 108 throw new IllegalArgumentException( 109 "Query on " + uri + " returns null result."); 110 } 111 112 try { 113 if ((c.getCount() != 1) || !c.moveToFirst()) { 114 throw new IllegalArgumentException( 115 "Query on " + uri + " returns 0 or multiple rows."); 116 } 117 118 String filePath; 119 if (ImageModel.isMmsUri(uri)) { 120 filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME)); 121 if (TextUtils.isEmpty(filePath)) { 122 filePath = c.getString( 123 c.getColumnIndexOrThrow(Part._DATA)); 124 } 125 mContentType = c.getString( 126 c.getColumnIndexOrThrow(Part.CONTENT_TYPE)); 127 } else { 128 filePath = c.getString( 129 c.getColumnIndexOrThrow(Images.Media.DATA)); 130 mContentType = c.getString( 131 c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); 132 } 133 mPath = filePath; 134 } finally { 135 c.close(); 136 } 137 } 138 139 private void decodeBoundsInfo() { 140 InputStream input = null; 141 try { 142 input = mContext.getContentResolver().openInputStream(mUri); 143 BitmapFactory.Options opt = new BitmapFactory.Options(); 144 opt.inJustDecodeBounds = true; 145 BitmapFactory.decodeStream(input, null, opt); 146 mWidth = opt.outWidth; 147 mHeight = opt.outHeight; 148 } catch (FileNotFoundException e) { 149 // Ignore 150 Log.e(TAG, "IOException caught while opening stream", e); 151 } finally { 152 if (null != input) { 153 try { 154 input.close(); 155 } catch (IOException e) { 156 // Ignore 157 Log.e(TAG, "IOException caught while closing stream", e); 158 } 159 } 160 } 161 } 162 163 public String getContentType() { 164 return mContentType; 165 } 166 167 public String getSrc() { 168 return mSrc; 169 } 170 171 public int getWidth() { 172 return mWidth; 173 } 174 175 public int getHeight() { 176 return mHeight; 177 } 178 179 public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) { 180 PduPart part = new PduPart(); 181 182 byte[] data = getResizedImageData(widthLimit, heightLimit, byteLimit); 183 if (data == null) { 184 if (LOCAL_LOGV) { 185 Log.v(TAG, "Resize image failed."); 186 } 187 return null; 188 } 189 190 part.setData(data); 191 part.setContentType(getContentType().getBytes()); 192 193 return part; 194 } 195 196 private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4; 197 198 private byte[] getResizedImageData(int widthLimit, int heightLimit, int byteLimit) { 199 int outWidth = mWidth; 200 int outHeight = mHeight; 201 202 int scaleFactor = 1; 203 while ((outWidth / scaleFactor > widthLimit) || (outHeight / scaleFactor > heightLimit)) { 204 scaleFactor *= 2; 205 } 206 207 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 208 Log.v(TAG, "getResizedImageData: wlimit=" + widthLimit + 209 ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit + 210 ", mWidth=" + mWidth + ", mHeight=" + mHeight + 211 ", initialScaleFactor=" + scaleFactor); 212 } 213 214 InputStream input = null; 215 try { 216 ByteArrayOutputStream os = null; 217 int attempts = 1; 218 219 do { 220 BitmapFactory.Options options = new BitmapFactory.Options(); 221 options.inSampleSize = scaleFactor; 222 input = mContext.getContentResolver().openInputStream(mUri); 223 int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY; 224 try { 225 Bitmap b = BitmapFactory.decodeStream(input, null, options); 226 if (b == null) { 227 return null; 228 } 229 if (options.outWidth > widthLimit || options.outHeight > heightLimit) { 230 // The decoder does not support the inSampleSize option. 231 // Scale the bitmap using Bitmap library. 232 int scaledWidth = outWidth / scaleFactor; 233 int scaledHeight = outHeight / scaleFactor; 234 235 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 236 Log.v(TAG, "getResizedImageData: retry scaling using " + 237 "Bitmap.createScaledBitmap: w=" + scaledWidth + 238 ", h=" + scaledHeight); 239 } 240 241 b = Bitmap.createScaledBitmap(b, outWidth / scaleFactor, 242 outHeight / scaleFactor, false); 243 if (b == null) { 244 return null; 245 } 246 } 247 248 // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY. 249 // In case that the image byte size is still too large reduce the quality in 250 // proportion to the desired byte size. Should the quality fall below 251 // MINIMUM_IMAGE_COMPRESSION_QUALITY skip a compression attempt and we will enter 252 // the next round with a smaller image to start with. 253 os = new ByteArrayOutputStream(); 254 b.compress(CompressFormat.JPEG, quality, os); 255 int jpgFileSize = os.size(); 256 if (jpgFileSize > byteLimit) { 257 int reducedQuality = quality * byteLimit / jpgFileSize; 258 if (reducedQuality >= MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) { 259 quality = reducedQuality; 260 261 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 262 Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality); 263 } 264 265 os = new ByteArrayOutputStream(); 266 b.compress(CompressFormat.JPEG, quality, os); 267 } 268 } 269 b.recycle(); // done with the bitmap, release the memory 270 } catch (java.lang.OutOfMemoryError e) { 271 Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try " 272 + " with smaller scale factor, cur scale factor: " + scaleFactor); 273 // fall through and keep trying with a smaller scale factor. 274 } 275 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 276 Log.v(TAG, "attempt=" + attempts 277 + " size=" + (os == null ? 0 : os.size()) 278 + " width=" + outWidth / scaleFactor 279 + " height=" + outHeight / scaleFactor 280 + " scaleFactor=" + scaleFactor 281 + " quality=" + quality); 282 } 283 scaleFactor *= 2; 284 attempts++; 285 } while ((os == null || os.size() > byteLimit) && attempts < NUMBER_OF_RESIZE_ATTEMPTS); 286 287 return os == null ? null : os.toByteArray(); 288 } catch (FileNotFoundException e) { 289 Log.e(TAG, e.getMessage(), e); 290 return null; 291 } catch (java.lang.OutOfMemoryError e) { 292 Log.e(TAG, e.getMessage(), e); 293 return null; 294 } finally { 295 if (input != null) { 296 try { 297 input.close(); 298 } catch (IOException e) { 299 Log.e(TAG, e.getMessage(), e); 300 } 301 } 302 } 303 } 304 } 305