• 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 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