• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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