UriImage.java revision 2c48c7894f1c4d4265bfe499e596fdd6fd3e9484
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
18package com.android.mms.ui;
19
20import com.android.mms.model.ImageModel;
21import com.android.mms.LogTag;
22
23import com.google.android.mms.ContentType;
24import com.google.android.mms.pdu.PduPart;
25import android.database.sqlite.SqliteWrapper;
26
27import android.content.Context;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
31import android.graphics.Bitmap.CompressFormat;
32import android.net.Uri;
33import android.provider.MediaStore.Images;
34import android.provider.Telephony.Mms.Part;
35import android.text.TextUtils;
36import android.util.Log;
37import android.webkit.MimeTypeMap;
38
39import java.io.ByteArrayOutputStream;
40import java.io.FileNotFoundException;
41import java.io.IOException;
42import java.io.InputStream;
43import java.io.OutputStream;
44
45public 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
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
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
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
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
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
198    public String getContentType() {
199        return mContentType;
200    }
201
202    public String getSrc() {
203        return mSrc;
204    }
205
206    public String getPath() {
207        return mPath;
208    }
209
210    public int getWidth() {
211        return mWidth;
212    }
213
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     */
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     */
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