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