• 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 android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.UriMatcher;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteException;
25 import android.database.sqlite.SqliteWrapper;
26 import android.graphics.Bitmap;
27 import android.graphics.Bitmap.CompressFormat;
28 import android.graphics.BitmapFactory;
29 import android.graphics.Matrix;
30 import android.net.Uri;
31 import android.provider.MediaStore;
32 import android.provider.MediaStore.Images;
33 import android.provider.Telephony.Mms.Part;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.webkit.MimeTypeMap;
37 
38 import com.android.mms.LogTag;
39 import com.android.mms.exif.ExifInterface;
40 import com.android.mms.model.ImageModel;
41 import com.google.android.mms.ContentType;
42 import com.google.android.mms.pdu.PduPart;
43 
44 import java.io.ByteArrayOutputStream;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 import java.io.InputStream;
48 
49 public class UriImage {
50     private static final String TAG = LogTag.TAG;
51     private static final boolean DEBUG = false;
52     private static final boolean LOCAL_LOGV = false;
53     private static final int MMS_PART_ID = 12;
54     private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
55     static {
56         sURLMatcher.addURI("mms", "part/#", MMS_PART_ID);
57     }
58 
59     private final Context mContext;
60     private final Uri mUri;
61     private String mContentType;
62     private String mPath;
63     private String mSrc;
64     private int mWidth;
65     private int mHeight;
66 
UriImage(Context context, Uri uri)67     public UriImage(Context context, Uri uri) {
68         if ((null == context) || (null == uri)) {
69             throw new IllegalArgumentException();
70         }
71 
72         String scheme = uri.getScheme();
73         if (scheme.equals("content")) {
74             initFromContentUri(context, uri);
75         } else if (uri.getScheme().equals("file")) {
76             initFromFile(context, uri);
77         }
78 
79         mContext = context;
80         mUri = uri;
81 
82         decodeBoundsInfo();
83 
84         if (LOCAL_LOGV) {
85             Log.v(TAG, "UriImage uri: " + uri + " mPath: " + mPath + " mWidth: " + mWidth +
86                     " mHeight: " + mHeight);
87         }
88     }
89 
initFromFile(Context context, Uri uri)90     private void initFromFile(Context context, Uri uri) {
91         mPath = uri.getPath();
92         MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
93         String extension = MimeTypeMap.getFileExtensionFromUrl(mPath);
94         if (TextUtils.isEmpty(extension)) {
95             // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle
96             // urlEncoded strings. Let's try one last time at finding the extension.
97             int dotPos = mPath.lastIndexOf('.');
98             if (0 <= dotPos) {
99                 extension = mPath.substring(dotPos + 1);
100             }
101         }
102         mContentType = mimeTypeMap.getMimeTypeFromExtension(extension);
103         // It's ok if mContentType is null. Eventually we'll show a toast telling the
104         // user the picture couldn't be attached.
105 
106         buildSrcFromPath();
107     }
108 
buildSrcFromPath()109     private void buildSrcFromPath() {
110         mSrc = mPath.substring(mPath.lastIndexOf('/') + 1);
111 
112         if(mSrc.startsWith(".") && mSrc.length() > 1) {
113             mSrc = mSrc.substring(1);
114         }
115 
116         // Some MMSCs appear to have problems with filenames
117         // containing a space.  So just replace them with
118         // underscores in the name, which is typically not
119         // visible to the user anyway.
120         mSrc = mSrc.replace(' ', '_');
121     }
122 
initFromContentUri(Context context, Uri uri)123     private void initFromContentUri(Context context, Uri uri) {
124         ContentResolver resolver = context.getContentResolver();
125         Cursor c = SqliteWrapper.query(context, resolver,
126                             uri, null, null, null, null);
127 
128         mSrc = null;
129         if (c == null) {
130             throw new IllegalArgumentException(
131                     "Query on " + uri + " returns null result.");
132         }
133 
134         try {
135             if ((c.getCount() != 1) || !c.moveToFirst()) {
136                 throw new IllegalArgumentException(
137                         "Query on " + uri + " returns 0 or multiple rows.");
138             }
139 
140             String filePath;
141             if (ImageModel.isMmsUri(uri)) {
142                 filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME));
143                 if (TextUtils.isEmpty(filePath)) {
144                     filePath = c.getString(
145                             c.getColumnIndexOrThrow(Part._DATA));
146                 }
147                 mContentType = c.getString(
148                         c.getColumnIndexOrThrow(Part.CONTENT_TYPE));
149             } else {
150                 filePath = uri.getPath();
151                 try {
152                     mContentType = c.getString(
153                             c.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); // mime_type
154                 } catch (IllegalArgumentException e) {
155                     try {
156                         mContentType = c.getString(c.getColumnIndexOrThrow("mimetype"));
157                     } catch (IllegalArgumentException ex) {
158                         mContentType = resolver.getType(uri);
159                         Log.v(TAG, "initFromContentUri: " + uri + ", getType => " + mContentType);
160                     }
161                 }
162 
163                 // use the original filename if possible
164                 int nameIndex = c.getColumnIndex(Images.Media.DISPLAY_NAME);
165                 if (nameIndex != -1) {
166                     mSrc = c.getString(nameIndex);
167                     if (!TextUtils.isEmpty(mSrc)) {
168                         // Some MMSCs appear to have problems with filenames
169                         // containing a space.  So just replace them with
170                         // underscores in the name, which is typically not
171                         // visible to the user anyway.
172                         mSrc = mSrc.replace(' ', '_');
173                     } else {
174                         mSrc = null;
175                     }
176                 }
177             }
178             mPath = filePath;
179             if (mSrc == null) {
180                 buildSrcFromPath();
181             }
182         } catch (IllegalArgumentException e) {
183             Log.e(TAG, "initFromContentUri couldn't load image uri: " + uri, e);
184         } finally {
185             c.close();
186         }
187     }
188 
decodeBoundsInfo()189     private void decodeBoundsInfo() {
190         InputStream input = null;
191         try {
192             input = mContext.getContentResolver().openInputStream(mUri);
193             BitmapFactory.Options opt = new BitmapFactory.Options();
194             opt.inJustDecodeBounds = true;
195             BitmapFactory.decodeStream(input, null, opt);
196             mWidth = opt.outWidth;
197             mHeight = opt.outHeight;
198         } catch (FileNotFoundException e) {
199             // Ignore
200             Log.e(TAG, "IOException caught while opening stream", e);
201         } finally {
202             if (null != input) {
203                 try {
204                     input.close();
205                 } catch (IOException e) {
206                     // Ignore
207                     Log.e(TAG, "IOException caught while closing stream", e);
208                 }
209             }
210         }
211     }
212 
getContentType()213     public String getContentType() {
214         return mContentType;
215     }
216 
getSrc()217     public String getSrc() {
218         return mSrc;
219     }
220 
getPath()221     public String getPath() {
222         return mPath;
223     }
224 
getWidth()225     public int getWidth() {
226         return mWidth;
227     }
228 
getHeight()229     public int getHeight() {
230         return mHeight;
231     }
232 
233     /**
234      * Get a version of this image resized to fit the given dimension and byte-size limits. Note
235      * that the content type of the resulting PduPart may not be the same as the content type of
236      * this UriImage; always call {@link PduPart#getContentType()} to get the new content type.
237      *
238      * @param widthLimit The width limit, in pixels
239      * @param heightLimit The height limit, in pixels
240      * @param byteLimit The binary size limit, in bytes
241      * @return A new PduPart containing the resized image data
242      */
getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit)243     public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) {
244         PduPart part = new PduPart();
245 
246         byte[] data =  getResizedImageData(mWidth, mHeight,
247                 widthLimit, heightLimit, byteLimit, mUri, mContext);
248         if (data == null) {
249             if (LOCAL_LOGV) {
250                 Log.v(TAG, "Resize image failed.");
251             }
252             return null;
253         }
254 
255         part.setData(data);
256         // getResizedImageData ALWAYS compresses to JPEG, regardless of the original content type
257         part.setContentType(ContentType.IMAGE_JPEG.getBytes());
258 
259         return part;
260     }
261 
262     private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4;
263 
264     /**
265      * Resize and recompress the image such that it fits the given limits. The resulting byte
266      * array contains an image in JPEG format, regardless of the original image's content type.
267      * @param widthLimit The width limit, in pixels
268      * @param heightLimit The height limit, in pixels
269      * @param byteLimit The binary size limit, in bytes
270      * @return A resized/recompressed version of this image, in JPEG format
271      */
getResizedImageData(int width, int height, int widthLimit, int heightLimit, int byteLimit, Uri uri, Context context)272     public static byte[] getResizedImageData(int width, int height,
273             int widthLimit, int heightLimit, int byteLimit, Uri uri, Context context) {
274         int outWidth = width;
275         int outHeight = height;
276 
277         float scaleFactor = 1.F;
278         while ((outWidth * scaleFactor > widthLimit) || (outHeight * scaleFactor > heightLimit)) {
279             scaleFactor *= .75F;
280         }
281 
282         int orientation = getOrientation(context, uri);
283 
284         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
285             Log.v(TAG, "getResizedBitmap: wlimit=" + widthLimit +
286                     ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit +
287                     ", width=" + width + ", height=" + height +
288                     ", initialScaleFactor=" + scaleFactor +
289                     ", uri=" + uri +
290                     ", orientation=" + orientation);
291         }
292 
293         InputStream input = null;
294         ByteArrayOutputStream os = null;
295         try {
296             int attempts = 1;
297             int sampleSize = 1;
298             BitmapFactory.Options options = new BitmapFactory.Options();
299             int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY;
300             Bitmap b = null;
301 
302             // In this loop, attempt to decode the stream with the best possible subsampling (we
303             // start with 1, which means no subsampling - get the original content) without running
304             // out of memory.
305             do {
306                 input = context.getContentResolver().openInputStream(uri);
307                 options.inSampleSize = sampleSize;
308                 try {
309                     b = BitmapFactory.decodeStream(input, null, options);
310                     if (b == null) {
311                         return null;    // Couldn't decode and it wasn't because of an exception,
312                                         // bail.
313                     }
314                 } catch (OutOfMemoryError e) {
315                     Log.w(TAG, "getResizedBitmap: img too large to decode (OutOfMemoryError), " +
316                             "may try with larger sampleSize. Curr sampleSize=" + sampleSize);
317                     sampleSize *= 2;    // works best as a power of two
318                     attempts++;
319                     continue;
320                 } finally {
321                     if (input != null) {
322                         try {
323                             input.close();
324                         } catch (IOException e) {
325                             Log.e(TAG, e.getMessage(), e);
326                         }
327                     }
328                 }
329             } while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
330 
331             if (b == null) {
332                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)
333                         && attempts >= NUMBER_OF_RESIZE_ATTEMPTS) {
334                     Log.v(TAG, "getResizedImageData: gave up after too many attempts to resize");
335                 }
336                 return null;
337             }
338 
339             boolean resultTooBig = true;
340             attempts = 1;   // reset count for second loop
341             // In this loop, we attempt to compress/resize the content to fit the given dimension
342             // and file-size limits.
343             do {
344                 try {
345                     if (options.outWidth > widthLimit || options.outHeight > heightLimit ||
346                             (os != null && os.size() > byteLimit)) {
347                         // The decoder does not support the inSampleSize option.
348                         // Scale the bitmap using Bitmap library.
349                         int scaledWidth = (int)(outWidth * scaleFactor);
350                         int scaledHeight = (int)(outHeight * scaleFactor);
351 
352                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
353                             Log.v(TAG, "getResizedImageData: retry scaling using " +
354                                     "Bitmap.createScaledBitmap: w=" + scaledWidth +
355                                     ", h=" + scaledHeight);
356                         }
357 
358                         b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false);
359                         if (b == null) {
360                             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
361                                 Log.v(TAG, "Bitmap.createScaledBitmap returned NULL!");
362                             }
363                             return null;
364                         }
365                     }
366 
367                     // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
368                     // In case that the image byte size is still too large reduce the quality in
369                     // proportion to the desired byte size.
370                     if (os != null) {
371                         try {
372                             os.close();
373                         } catch (IOException e) {
374                             Log.e(TAG, e.getMessage(), e);
375                         }
376                     }
377                     os = new ByteArrayOutputStream();
378                     b.compress(CompressFormat.JPEG, quality, os);
379                     int jpgFileSize = os.size();
380                     if (jpgFileSize > byteLimit) {
381                         quality = (quality * byteLimit) / jpgFileSize;  // watch for int division!
382                         if (quality < MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
383                             quality = MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY;
384                         }
385 
386                         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
387                             Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
388                         }
389 
390                         if (os != null) {
391                             try {
392                                 os.close();
393                             } catch (IOException e) {
394                                 Log.e(TAG, e.getMessage(), e);
395                             }
396                         }
397                         os = new ByteArrayOutputStream();
398                         b.compress(CompressFormat.JPEG, quality, os);
399                     }
400                 } catch (java.lang.OutOfMemoryError e) {
401                     Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try "
402                             + " with smaller scale factor, cur scale factor: " + scaleFactor);
403                     // fall through and keep trying with a smaller scale factor.
404                 }
405                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
406                     Log.v(TAG, "attempt=" + attempts
407                             + " size=" + (os == null ? 0 : os.size())
408                             + " width=" + outWidth * scaleFactor
409                             + " height=" + outHeight * scaleFactor
410                             + " scaleFactor=" + scaleFactor
411                             + " quality=" + quality);
412                 }
413                 scaleFactor *= .75F;
414                 attempts++;
415                 resultTooBig = os == null || os.size() > byteLimit;
416             } while (resultTooBig && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
417             if (!resultTooBig && orientation != 0) {
418                 // Rotate the final bitmap if we need to.
419                 try {
420                     b = UriImage.rotateBitmap(b, orientation);
421                     os = new ByteArrayOutputStream();
422                     b.compress(CompressFormat.JPEG, quality, os);
423                     resultTooBig = os == null || os.size() > byteLimit;
424                 } catch (java.lang.OutOfMemoryError e) {
425                     Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError)");
426                     if (os == null) {
427                         return null;
428                     }
429                 }
430             }
431 
432             b.recycle();        // done with the bitmap, release the memory
433             if (Log.isLoggable(LogTag.APP, Log.VERBOSE) && resultTooBig) {
434                 Log.v(TAG, "getResizedImageData returning NULL because the result is too big: " +
435                         " requested max: " + byteLimit + " actual: " + os.size());
436             }
437 
438             return resultTooBig ? null : os.toByteArray();
439         } catch (FileNotFoundException e) {
440             Log.e(TAG, e.getMessage(), e);
441             return null;
442         } catch (java.lang.OutOfMemoryError e) {
443             Log.e(TAG, e.getMessage(), e);
444             return null;
445         } finally {
446             if (input != null) {
447                 try {
448                     input.close();
449                 } catch (IOException e) {
450                     Log.e(TAG, e.getMessage(), e);
451                 }
452             }
453             if (os != null) {
454                 try {
455                     os.close();
456                 } catch (IOException e) {
457                     Log.e(TAG, e.getMessage(), e);
458                 }
459             }
460         }
461     }
462 
463     /**
464      * Bitmap rotation method
465      *
466      * @param bitmap The input bitmap
467      * @param degrees The rotation angle
468      */
rotateBitmap(Bitmap bitmap, int degrees)469     public static Bitmap rotateBitmap(Bitmap bitmap, int degrees) {
470         if (degrees != 0 && bitmap != null) {
471             final Matrix m = new Matrix();
472             final int w = bitmap.getWidth();
473             final int h = bitmap.getHeight();
474             m.setRotate(degrees, (float) w / 2, (float) h / 2);
475 
476             try {
477                 final Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, m, true);
478                 if (bitmap != rotatedBitmap && rotatedBitmap != null) {
479                     bitmap.recycle();
480                     bitmap = rotatedBitmap;
481                 }
482             } catch (OutOfMemoryError ex) {
483                 Log.e(TAG, "OOM in rotateBitmap", ex);
484                 // We have no memory to rotate. Return the original bitmap.
485             }
486         }
487 
488         return bitmap;
489     }
490 
491     /**
492      * Returns the number of degrees to rotate the picture, based on the orientation tag in
493      * the exif data or the orientation column in the database. If there's no tag or column,
494      * 0 degrees is returned.
495      *
496      * @param context Used to get the ContentResolver
497      * @param uri Path to the image
498      */
getOrientation(Context context, Uri uri)499     public static int getOrientation(Context context, Uri uri) {
500         long dur = System.currentTimeMillis();
501         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme()) ||
502                 sURLMatcher.match(uri) == MMS_PART_ID) {
503             // If the uri is a file or an mms part, we have to look at the exif data in the
504             // file for the orientation because there is no column in the db for the orientation.
505             try {
506                 InputStream inputStream = context.getContentResolver().openInputStream(uri);
507                 ExifInterface exif = new ExifInterface();
508                 try {
509                     exif.readExif(inputStream);
510                     Integer val = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION);
511                     if (val == null){
512                         return 0;
513                     }
514                     int orientation =
515                             ExifInterface.getRotationForOrientationValue(val.shortValue());
516                     return orientation;
517                 } catch (IOException e) {
518                     Log.w(TAG, "Failed to read EXIF orientation", e);
519                 } finally {
520                     if (inputStream != null) {
521                         try {
522                             inputStream.close();
523                         } catch (IOException e) {
524                         }
525                     }
526                 }
527             } catch (FileNotFoundException e) {
528                 Log.e(TAG, "Can't open uri: " + uri, e);
529             } finally {
530                 dur = System.currentTimeMillis() - dur;
531                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
532                     Log.v(TAG, "UriImage.getOrientation (exif path) took: " + dur + " ms");
533                 }
534             }
535         } else {
536             // Try to get the orientation from the ORIENTATION column in the database. This is much
537             // faster than reading all the exif tags from the file.
538             Cursor cursor = null;
539             try {
540                 cursor = context.getContentResolver().query(uri,
541                         new String[] {
542                             MediaStore.Images.ImageColumns.ORIENTATION
543                         },
544                         null, null, null);
545                 if (cursor.moveToNext()) {
546                     int ori = cursor.getInt(0);
547                     return ori;
548                 }
549             } catch (SQLiteException e) {
550             } catch (IllegalArgumentException e) {
551             } finally {
552                 if (cursor != null) {
553                     cursor.close();
554                 }
555                 dur = System.currentTimeMillis() - dur;
556                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
557                     Log.v(TAG, "UriImage.getOrientation (db column path) took: " + dur + " ms");
558                 }
559             }
560         }
561         return 0;
562     }
563 }
564