UriImage.java revision 3d8877fa3e07c019d1779793ca0081beca177b6a
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;
43
44public 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
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
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
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
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
168    public String getContentType() {
169        return mContentType;
170    }
171
172    public String getSrc() {
173        return mSrc;
174    }
175
176    public int getWidth() {
177        return mWidth;
178    }
179
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     */
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     */
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                    if (b == null) {
257                        return null;    // Couldn't decode and it wasn't because of an exception,
258                                        // bail.
259                    }
260                } catch (OutOfMemoryError e) {
261                    Log.w(TAG, "getResizedImageData: img too large to decode (OutOfMemoryError), " +
262                            "may try with larger sampleSize. Curr sampleSize=" + sampleSize);
263                    sampleSize *= 2;    // works best as a power of two
264                    attempts++;
265                    continue;
266                } finally {
267                    if (input != null) {
268                        try {
269                            input.close();
270                        } catch (IOException e) {
271                            Log.e(TAG, e.getMessage(), e);
272                        }
273                    }
274                }
275            } while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
276
277            if (b == null) {
278                return null;
279            }
280
281            attempts = 1;   // reset count for second loop
282            // In this loop, we attempt to compress/resize the content to fit the given dimension
283            // and file-size limits.
284            do {
285                try {
286                    if (options.outWidth > widthLimit || options.outHeight > heightLimit ||
287                            (os != null && os.size() > byteLimit)) {
288                        // The decoder does not support the inSampleSize option.
289                        // Scale the bitmap using Bitmap library.
290                        int scaledWidth = (int)(outWidth * scaleFactor);
291                        int scaledHeight = (int)(outHeight * scaleFactor);
292
293                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
294                            Log.v(TAG, "getResizedImageData: retry scaling using " +
295                                    "Bitmap.createScaledBitmap: w=" + scaledWidth +
296                                    ", h=" + scaledHeight);
297                        }
298
299                        b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false);
300                        if (b == null) {
301                            return null;
302                        }
303                    }
304
305                    // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
306                    // In case that the image byte size is still too large reduce the quality in
307                    // proportion to the desired byte size.
308                    os = new ByteArrayOutputStream();
309                    b.compress(CompressFormat.JPEG, quality, os);
310                    int jpgFileSize = os.size();
311                    if (jpgFileSize > byteLimit) {
312                        quality = (quality * byteLimit) / jpgFileSize;  // watch for int division!
313                        if (quality < MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
314                            quality = MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY;
315                        }
316
317                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
318                            Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
319                        }
320
321                        os = new ByteArrayOutputStream();
322                        b.compress(CompressFormat.JPEG, quality, os);
323                    }
324                } catch (java.lang.OutOfMemoryError e) {
325                    Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try "
326                            + " with smaller scale factor, cur scale factor: " + scaleFactor);
327                    // fall through and keep trying with a smaller scale factor.
328                }
329                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
330                    Log.v(TAG, "attempt=" + attempts
331                            + " size=" + (os == null ? 0 : os.size())
332                            + " width=" + outWidth * scaleFactor
333                            + " height=" + outHeight * scaleFactor
334                            + " scaleFactor=" + scaleFactor
335                            + " quality=" + quality);
336                }
337                scaleFactor *= .75F;
338                attempts++;
339            } while ((os == null || os.size() > byteLimit) && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
340            b.recycle();        // done with the bitmap, release the memory
341
342            return os == null ? null : os.toByteArray();
343        } catch (FileNotFoundException e) {
344            Log.e(TAG, e.getMessage(), e);
345            return null;
346        } catch (java.lang.OutOfMemoryError e) {
347            Log.e(TAG, e.getMessage(), e);
348            return null;
349        }
350    }
351}
352