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.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        int period = src.lastIndexOf(".");
193        byte[] contentId = period != -1 ? src.substring(0, period).getBytes() : srcBytes;
194        part.setContentId(contentId);
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        }
216
217        InputStream input = null;
218        try {
219            ByteArrayOutputStream os = null;
220            int attempts = 1;
221
222            do {
223                BitmapFactory.Options options = new BitmapFactory.Options();
224                options.inSampleSize = scaleFactor;
225                input = mContext.getContentResolver().openInputStream(mUri);
226                int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY;
227                try {
228                    Bitmap b = BitmapFactory.decodeStream(input, null, options);
229                    if (b == null) {
230                        return null;
231                    }
232                    if (options.outWidth > widthLimit || options.outHeight > heightLimit) {
233                        // The decoder does not support the inSampleSize option.
234                        // Scale the bitmap using Bitmap library.
235                        int scaledWidth = outWidth / scaleFactor;
236                        int scaledHeight = outHeight / scaleFactor;
237
238                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
239                            Log.v(TAG, "getResizedImageData: retry scaling using " +
240                                    "Bitmap.createScaledBitmap: w=" + scaledWidth +
241                                    ", h=" + scaledHeight);
242                        }
243
244                        b = Bitmap.createScaledBitmap(b, outWidth / scaleFactor,
245                                outHeight / scaleFactor, false);
246                        if (b == null) {
247                            return null;
248                        }
249                    }
250
251                    // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
252                    // In case that the image byte size is still too large reduce the quality in
253                    // proportion to the desired byte size. Should the quality fall below
254                    // MINIMUM_IMAGE_COMPRESSION_QUALITY skip a compression attempt and we will enter
255                    // the next round with a smaller image to start with.
256                    os = new ByteArrayOutputStream();
257                    b.compress(CompressFormat.JPEG, quality, os);
258                    int jpgFileSize = os.size();
259                    if (jpgFileSize > byteLimit) {
260                        int reducedQuality = quality * byteLimit / jpgFileSize;
261                        if (reducedQuality >= MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
262                            quality = reducedQuality;
263
264                            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
265                                Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
266                            }
267
268                            os = new ByteArrayOutputStream();
269                            b.compress(CompressFormat.JPEG, quality, os);
270                        }
271                    }
272                } catch (java.lang.OutOfMemoryError e) {
273                    Log.e(TAG, e.getMessage(), e);
274                    // fall through and keep trying with a smaller scale factor.
275                }
276                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
277                    Log.v(TAG, "attempt=" + attempts
278                            + " size=" + (os == null ? 0 : os.size())
279                            + " width=" + outWidth / scaleFactor
280                            + " height=" + outHeight / scaleFactor
281                            + " scaleFactor=" + scaleFactor
282                            + " quality=" + quality);
283                }
284                scaleFactor *= 2;
285                attempts++;
286            } while ((os == null || os.size() > byteLimit) && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
287
288            return os == null ? null : os.toByteArray();
289        } catch (FileNotFoundException e) {
290            Log.e(TAG, e.getMessage(), e);
291            return null;
292        } finally {
293            if (input != null) {
294                try {
295                    input.close();
296                } catch (IOException e) {
297                    Log.e(TAG, e.getMessage(), e);
298                }
299            }
300        }
301    }
302}
303