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