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 java.io.ByteArrayOutputStream;
21import java.io.FileNotFoundException;
22import java.io.IOException;
23import java.io.InputStream;
24
25import android.content.ContentResolver;
26import android.content.Context;
27import android.database.Cursor;
28import android.database.sqlite.SqliteWrapper;
29import android.graphics.Bitmap;
30import android.graphics.Bitmap.CompressFormat;
31import android.graphics.BitmapFactory;
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 com.android.mms.LogTag;
40import com.android.mms.model.ImageModel;
41import com.google.android.mms.ContentType;
42import com.google.android.mms.pdu.PduPart;
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        mContext = context;
70        mUri = uri;
71
72        decodeBoundsInfo();
73
74        if (LOCAL_LOGV) {
75            Log.v(TAG, "UriImage uri: " + uri + " mPath: " + mPath + " mWidth: " + mWidth +
76                    " mHeight: " + mHeight);
77        }
78    }
79
80    private void initFromFile(Context context, Uri uri) {
81        mPath = uri.getPath();
82        MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
83        String extension = MimeTypeMap.getFileExtensionFromUrl(mPath);
84        if (TextUtils.isEmpty(extension)) {
85            // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle
86            // urlEncoded strings. Let's try one last time at finding the extension.
87            int dotPos = mPath.lastIndexOf('.');
88            if (0 <= dotPos) {
89                extension = mPath.substring(dotPos + 1);
90            }
91        }
92        mContentType = mimeTypeMap.getMimeTypeFromExtension(extension);
93        // It's ok if mContentType is null. Eventually we'll show a toast telling the
94        // user the picture couldn't be attached.
95
96        buildSrcFromPath();
97    }
98
99    private void buildSrcFromPath() {
100        mSrc = mPath.substring(mPath.lastIndexOf('/') + 1);
101
102        if(mSrc.startsWith(".") && mSrc.length() > 1) {
103            mSrc = mSrc.substring(1);
104        }
105
106        // Some MMSCs appear to have problems with filenames
107        // containing a space.  So just replace them with
108        // underscores in the name, which is typically not
109        // visible to the user anyway.
110        mSrc = mSrc.replace(' ', '_');
111    }
112
113    private void initFromContentUri(Context context, Uri uri) {
114        ContentResolver resolver = context.getContentResolver();
115        Cursor c = SqliteWrapper.query(context, resolver,
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                    try {
146                        mContentType = c.getString(c.getColumnIndexOrThrow("mimetype"));
147                    } catch (IllegalArgumentException ex) {
148                        mContentType = resolver.getType(uri);
149                        Log.v(TAG, "initFromContentUri: " + uri + ", getType => " + mContentType);
150                    }
151                }
152
153                // use the original filename if possible
154                int nameIndex = c.getColumnIndex(Images.Media.DISPLAY_NAME);
155                if (nameIndex != -1) {
156                    mSrc = c.getString(nameIndex);
157                    if (!TextUtils.isEmpty(mSrc)) {
158                        // Some MMSCs appear to have problems with filenames
159                        // containing a space.  So just replace them with
160                        // underscores in the name, which is typically not
161                        // visible to the user anyway.
162                        mSrc = mSrc.replace(' ', '_');
163                    } else {
164                        mSrc = null;
165                    }
166                }
167            }
168            mPath = filePath;
169            if (mSrc == null) {
170                buildSrcFromPath();
171            }
172        } catch (IllegalArgumentException e) {
173            Log.e(TAG, "initFromContentUri couldn't load image uri: " + uri, e);
174        } finally {
175            c.close();
176        }
177    }
178
179    private void decodeBoundsInfo() {
180        InputStream input = null;
181        try {
182            input = mContext.getContentResolver().openInputStream(mUri);
183            BitmapFactory.Options opt = new BitmapFactory.Options();
184            opt.inJustDecodeBounds = true;
185            BitmapFactory.decodeStream(input, null, opt);
186            mWidth = opt.outWidth;
187            mHeight = opt.outHeight;
188        } catch (FileNotFoundException e) {
189            // Ignore
190            Log.e(TAG, "IOException caught while opening stream", e);
191        } finally {
192            if (null != input) {
193                try {
194                    input.close();
195                } catch (IOException e) {
196                    // Ignore
197                    Log.e(TAG, "IOException caught while closing stream", e);
198                }
199            }
200        }
201    }
202
203    public String getContentType() {
204        return mContentType;
205    }
206
207    public String getSrc() {
208        return mSrc;
209    }
210
211    public String getPath() {
212        return mPath;
213    }
214
215    public int getWidth() {
216        return mWidth;
217    }
218
219    public int getHeight() {
220        return mHeight;
221    }
222
223    /**
224     * Get a version of this image resized to fit the given dimension and byte-size limits. Note
225     * that the content type of the resulting PduPart may not be the same as the content type of
226     * this UriImage; always call {@link PduPart#getContentType()} to get the new content type.
227     *
228     * @param widthLimit The width limit, in pixels
229     * @param heightLimit The height limit, in pixels
230     * @param byteLimit The binary size limit, in bytes
231     * @return A new PduPart containing the resized image data
232     */
233    public PduPart getResizedImageAsPart(int widthLimit, int heightLimit, int byteLimit) {
234        PduPart part = new PduPart();
235
236        byte[] data =  getResizedImageData(mWidth, mHeight,
237                widthLimit, heightLimit, byteLimit, mUri, mContext);
238        if (data == null) {
239            if (LOCAL_LOGV) {
240                Log.v(TAG, "Resize image failed.");
241            }
242            return null;
243        }
244
245        part.setData(data);
246        // getResizedImageData ALWAYS compresses to JPEG, regardless of the original content type
247        part.setContentType(ContentType.IMAGE_JPEG.getBytes());
248
249        return part;
250    }
251
252    private static final int NUMBER_OF_RESIZE_ATTEMPTS = 4;
253
254    /**
255     * Resize and recompress the image such that it fits the given limits. The resulting byte
256     * array contains an image in JPEG format, regardless of the original image's content type.
257     * @param widthLimit The width limit, in pixels
258     * @param heightLimit The height limit, in pixels
259     * @param byteLimit The binary size limit, in bytes
260     * @return A resized/recompressed version of this image, in JPEG format
261     */
262    public static byte[] getResizedImageData(int width, int height,
263            int widthLimit, int heightLimit, int byteLimit, Uri uri, Context context) {
264        int outWidth = width;
265        int outHeight = height;
266
267        float scaleFactor = 1.F;
268        while ((outWidth * scaleFactor > widthLimit) || (outHeight * scaleFactor > heightLimit)) {
269            scaleFactor *= .75F;
270        }
271
272        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
273            Log.v(TAG, "getResizedBitmap: wlimit=" + widthLimit +
274                    ", hlimit=" + heightLimit + ", sizeLimit=" + byteLimit +
275                    ", width=" + width + ", height=" + height +
276                    ", initialScaleFactor=" + scaleFactor +
277                    ", uri=" + uri);
278        }
279
280        InputStream input = null;
281        try {
282            ByteArrayOutputStream os = null;
283            int attempts = 1;
284            int sampleSize = 1;
285            BitmapFactory.Options options = new BitmapFactory.Options();
286            int quality = MessageUtils.IMAGE_COMPRESSION_QUALITY;
287            Bitmap b = null;
288
289            // In this loop, attempt to decode the stream with the best possible subsampling (we
290            // start with 1, which means no subsampling - get the original content) without running
291            // out of memory.
292            do {
293                input = context.getContentResolver().openInputStream(uri);
294                options.inSampleSize = sampleSize;
295                try {
296                    b = BitmapFactory.decodeStream(input, null, options);
297                    if (b == null) {
298                        return null;    // Couldn't decode and it wasn't because of an exception,
299                                        // bail.
300                    }
301                } catch (OutOfMemoryError e) {
302                    Log.w(TAG, "getResizedBitmap: img too large to decode (OutOfMemoryError), " +
303                            "may try with larger sampleSize. Curr sampleSize=" + sampleSize);
304                    sampleSize *= 2;    // works best as a power of two
305                    attempts++;
306                    continue;
307                } finally {
308                    if (input != null) {
309                        try {
310                            input.close();
311                        } catch (IOException e) {
312                            Log.e(TAG, e.getMessage(), e);
313                        }
314                    }
315                }
316            } while (b == null && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
317
318            if (b == null) {
319                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)
320                        && attempts >= NUMBER_OF_RESIZE_ATTEMPTS) {
321                    Log.v(TAG, "getResizedImageData: gave up after too many attempts to resize");
322                }
323                return null;
324            }
325
326            boolean resultTooBig = true;
327            attempts = 1;   // reset count for second loop
328            // In this loop, we attempt to compress/resize the content to fit the given dimension
329            // and file-size limits.
330            do {
331                try {
332                    if (options.outWidth > widthLimit || options.outHeight > heightLimit ||
333                            (os != null && os.size() > byteLimit)) {
334                        // The decoder does not support the inSampleSize option.
335                        // Scale the bitmap using Bitmap library.
336                        int scaledWidth = (int)(outWidth * scaleFactor);
337                        int scaledHeight = (int)(outHeight * scaleFactor);
338
339                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
340                            Log.v(TAG, "getResizedImageData: retry scaling using " +
341                                    "Bitmap.createScaledBitmap: w=" + scaledWidth +
342                                    ", h=" + scaledHeight);
343                        }
344
345                        b = Bitmap.createScaledBitmap(b, scaledWidth, scaledHeight, false);
346                        if (b == null) {
347                            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
348                                Log.v(TAG, "Bitmap.createScaledBitmap returned NULL!");
349                            }
350                            return null;
351                        }
352                    }
353
354                    // Compress the image into a JPG. Start with MessageUtils.IMAGE_COMPRESSION_QUALITY.
355                    // In case that the image byte size is still too large reduce the quality in
356                    // proportion to the desired byte size.
357                    os = new ByteArrayOutputStream();
358                    b.compress(CompressFormat.JPEG, quality, os);
359                    int jpgFileSize = os.size();
360                    if (jpgFileSize > byteLimit) {
361                        quality = (quality * byteLimit) / jpgFileSize;  // watch for int division!
362                        if (quality < MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY) {
363                            quality = MessageUtils.MINIMUM_IMAGE_COMPRESSION_QUALITY;
364                        }
365
366                        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
367                            Log.v(TAG, "getResizedImageData: compress(2) w/ quality=" + quality);
368                        }
369
370                        os = new ByteArrayOutputStream();
371                        b.compress(CompressFormat.JPEG, quality, os);
372                    }
373                } catch (java.lang.OutOfMemoryError e) {
374                    Log.w(TAG, "getResizedImageData - image too big (OutOfMemoryError), will try "
375                            + " with smaller scale factor, cur scale factor: " + scaleFactor);
376                    // fall through and keep trying with a smaller scale factor.
377                }
378                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
379                    Log.v(TAG, "attempt=" + attempts
380                            + " size=" + (os == null ? 0 : os.size())
381                            + " width=" + outWidth * scaleFactor
382                            + " height=" + outHeight * scaleFactor
383                            + " scaleFactor=" + scaleFactor
384                            + " quality=" + quality);
385                }
386                scaleFactor *= .75F;
387                attempts++;
388                resultTooBig = os == null || os.size() > byteLimit;
389            } while (resultTooBig && attempts < NUMBER_OF_RESIZE_ATTEMPTS);
390            b.recycle();        // done with the bitmap, release the memory
391            if (Log.isLoggable(LogTag.APP, Log.VERBOSE) && resultTooBig) {
392                Log.v(TAG, "getResizedImageData returning NULL because the result is too big: " +
393                        " requested max: " + byteLimit + " actual: " + os.size());
394            }
395
396            return resultTooBig ? null : os.toByteArray();
397        } catch (FileNotFoundException e) {
398            Log.e(TAG, e.getMessage(), e);
399            return null;
400        } catch (java.lang.OutOfMemoryError e) {
401            Log.e(TAG, e.getMessage(), e);
402            return null;
403        }
404    }
405}
406