UriImage.java revision b0664c5822ee07642617a73c932b02a2636a2223
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;
22import com.google.android.mms.pdu.PduPart;
23import android.database.sqlite.SqliteWrapper;
24
25import android.content.Context;
26import android.database.Cursor;
27import android.graphics.Bitmap;
28import android.graphics.BitmapFactory;
29import android.graphics.Bitmap.CompressFormat;
30import android.net.Uri;
31import android.provider.MediaStore.Images;
32import android.provider.Telephony.Mms.Part;
33import android.text.TextUtils;
34import android.util.Log;
35import android.webkit.MimeTypeMap;
36
37import java.io.ByteArrayOutputStream;
38import java.io.FileNotFoundException;
39import java.io.IOException;
40import java.io.InputStream;
41
42public class UriImage {
43    private static final String TAG = "Mms/image";
44    private static final boolean DEBUG = false;
45    private static final boolean LOCAL_LOGV = false;
46
47    private final Context mContext;
48    private final Uri mUri;
49    private String mContentType;
50    private String mPath;
51    private String mSrc;
52    private int mWidth;
53    private int mHeight;
54
55    public UriImage(Context context, Uri uri) {
56        if ((null == context) || (null == uri)) {
57            throw new IllegalArgumentException();
58        }
59
60        String scheme = uri.getScheme();
61        if (scheme.equals("content")) {
62            initFromContentUri(context, uri);
63        } else if (uri.getScheme().equals("file")) {
64            initFromFile(context, uri);
65        }
66
67        mSrc = mPath.substring(mPath.lastIndexOf('/') + 1);
68
69        if(mSrc.startsWith(".") && mSrc.length() > 1) {
70            mSrc = mSrc.substring(1);
71        }
72
73        // Some MMSCs appear to have problems with filenames
74        // containing a space.  So just replace them with
75        // underscores in the name, which is typically not
76        // visible to the user anyway.
77        mSrc = mSrc.replace(' ', '_');
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
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
107    private void initFromContentUri(Context context, Uri uri) {
108        Cursor c = SqliteWrapper.query(context, context.getContentResolver(),
109                            uri, null, null, null, null);
110
111        if (c == null) {
112            throw new IllegalArgumentException(
113                    "Query on " + uri + " returns null result.");
114        }
115
116        try {
117            if ((c.getCount() != 1) || !c.moveToFirst()) {
118                throw new IllegalArgumentException(
119                        "Query on " + uri + " returns 0 or multiple rows.");
120            }
121
122            String filePath;
123            if (ImageModel.isMmsUri(uri)) {
124                filePath = c.getString(c.getColumnIndexOrThrow(Part.FILENAME));
125                if (TextUtils.isEmpty(filePath)) {
126                    filePath = c.getString(
127                            c.getColumnIndexOrThrow(Part._DATA));
128                }
129                mContentType = c.getString(
130                        c.getColumnIndexOrThrow(Part.CONTENT_TYPE));
131            } else {
132                filePath = uri.getPath();
133                mContentType = c.getString(
134                        c.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
135            }
136            mPath = filePath;
137        } finally {
138            c.close();
139        }
140    }
141
142    private void decodeBoundsInfo() {
143        InputStream input = null;
144        try {
145            input = mContext.getContentResolver().openInputStream(mUri);
146            BitmapFactory.Options opt = new BitmapFactory.Options();
147            opt.inJustDecodeBounds = true;
148            BitmapFactory.decodeStream(input, null, opt);
149            mWidth = opt.outWidth;
150            mHeight = opt.outHeight;
151        } catch (FileNotFoundException e) {
152            // Ignore
153            Log.e(TAG, "IOException caught while opening stream", e);
154        } finally {
155            if (null != input) {
156                try {
157                    input.close();
158                } catch (IOException e) {
159                    // Ignore
160                    Log.e(TAG, "IOException caught while closing stream", e);
161                }
162            }
163        }
164    }
165
166    public String getContentType() {
167        return mContentType;
168    }
169
170    public String getSrc() {
171        return mSrc;
172    }
173
174    public int getWidth() {
175        return mWidth;
176    }
177
178    public int getHeight() {
179        return mHeight;
180    }
181
182    public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) {
183        PduPart part = new PduPart();
184
185        byte[] data = getResizedImageData(widthLimit, heightLimit, byteLimit);
186        if (data == null) {
187            if (LOCAL_LOGV) {
188                Log.v(TAG, "Resize image failed.");
189            }
190            return null;
191        }
192
193        part.setData(data);
194        part.setContentType(getContentType().getBytes());
195
196        return part;
197    }
198
199    private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4;
200
201    private byte[] getResizedImageData(int widthLimit, int heightLimit, int byteLimit) {
202        int outWidth = mWidth;
203        int outHeight = mHeight;
204
205        int scaleFactor = 1;
206        while ((outWidth / scaleFactor > widthLimit) || (outHeight / scaleFactor > heightLimit)) {
207            scaleFactor *= 2;
208        }
209
210        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
211            Log.v(TAG, "getResizedImageData: wlimit=" + widthLimit +
212                    ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit +
213                    ", mWidth=" + mWidth + ", mHeight=" + mHeight +
214                    ", initialScaleFactor=" + scaleFactor +
215                    ", mUri=" + mUri);
216        }
217
218        InputStream input = null;
219        try {
220            ByteArrayOutputStream os = null;
221            int attempts = 1;
222
223            do {
224                BitmapFactory.Options options = new BitmapFactory.Options();
225                options.inSampleSize = scaleFactor;
226                input = mContext.getContentResolver().openInputStream(mUri);
227                int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY;
228                try {
229                    Bitmap b = BitmapFactory.decodeStream(input, null, options);
230                    if (b == null) {
231                        return null;
232                    }
233                    if (options.outWidth > widthLimit || options.outHeight > heightLimit) {
234                        // The decoder does not support the inSampleSize option.
235                        // Scale the bitmap using Bitmap library.
236                        int scaledWidth = outWidth / scaleFactor;
237                        int scaledHeight = outHeight / scaleFactor;
238
239                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
240                            Log.v(TAG, "getResizedImageData: retry scaling using " +
241                                    "Bitmap.createScaledBitmap: w=" + scaledWidth +
242                                    ", h=" + scaledHeight);
243                        }
244
245                        b = Bitmap.createScaledBitmap(b, outWidth / scaleFactor,
246                                outHeight / scaleFactor, false);
247                        if (b == null) {
248                            return null;
249                        }
250                    }
251
252                    // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
253                    // In case that the image byte size is still too large reduce the quality in
254                    // proportion to the desired byte size. Should the quality fall below
255                    // MINIMUM_IMAGE_COMPRESSION_QUALITY skip a compression attempt and we will enter
256                    // the next round with a smaller image to start with.
257                    os = new ByteArrayOutputStream();
258                    b.compress(CompressFormat.JPEG, quality, os);
259                    int jpgFileSize = os.size();
260                    if (jpgFileSize > byteLimit) {
261                        int reducedQuality = quality * byteLimit / jpgFileSize;
262                        if (reducedQuality >= MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
263                            quality = reducedQuality;
264
265                            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
266                                Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
267                            }
268
269                            os = new ByteArrayOutputStream();
270                            b.compress(CompressFormat.JPEG, quality, os);
271                        }
272                    }
273                    b.recycle();        // done with the bitmap, release the memory
274                } catch (java.lang.OutOfMemoryError e) {
275                    Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try "
276                            + " with smaller scale factor, cur scale factor: " + scaleFactor);
277                    // fall through and keep trying with a smaller scale factor.
278                }
279                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
280                    Log.v(TAG, "attempt=" + attempts
281                            + " size=" + (os == null ? 0 : os.size())
282                            + " width=" + outWidth / scaleFactor
283                            + " height=" + outHeight / scaleFactor
284                            + " scaleFactor=" + scaleFactor
285                            + " quality=" + quality);
286                }
287                scaleFactor *= 2;
288                attempts++;
289            } while ((os == null || os.size() > byteLimit) && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
290
291            return os == null ? null : os.toByteArray();
292        } catch (FileNotFoundException e) {
293            Log.e(TAG, e.getMessage(), e);
294            return null;
295        } catch (java.lang.OutOfMemoryError e) {
296            Log.e(TAG, e.getMessage(), e);
297            return null;
298        } finally {
299            if (input != null) {
300                try {
301                    input.close();
302                } catch (IOException e) {
303                    Log.e(TAG, e.getMessage(), e);
304                }
305            }
306        }
307    }
308}
309