Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2011 Google Inc.
      3  * Licensed to 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 
     18 package com.android.ex.photo.util;
     19 
     20 import android.content.ContentResolver;
     21 import android.graphics.Bitmap;
     22 import android.graphics.BitmapFactory;
     23 import android.graphics.Matrix;
     24 import android.graphics.Point;
     25 import android.graphics.Rect;
     26 import android.net.Uri;
     27 import android.os.Build;
     28 import android.util.Base64;
     29 import android.util.Log;
     30 
     31 import com.android.ex.photo.PhotoViewActivity;
     32 import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
     33 
     34 import java.io.ByteArrayInputStream;
     35 import java.io.ByteArrayOutputStream;
     36 import java.io.FileNotFoundException;
     37 import java.io.IOException;
     38 import java.io.InputStream;
     39 import java.net.MalformedURLException;
     40 import java.net.URL;
     41 import java.util.regex.Pattern;
     42 
     43 
     44 /**
     45  * Image utilities
     46  */
     47 public class ImageUtils {
     48     // Logging
     49     private static final String TAG = "ImageUtils";
     50 
     51     /** Minimum class memory class to use full-res photos */
     52     private final static long MIN_NORMAL_CLASS = 32;
     53     /** Minimum class memory class to use small photos */
     54     private final static long MIN_SMALL_CLASS = 24;
     55 
     56     private static final String BASE64_URI_PREFIX = "base64,";
     57     private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
     58 
     59     public static enum ImageSize {
     60         EXTRA_SMALL,
     61         SMALL,
     62         NORMAL,
     63     }
     64 
     65     public static final ImageSize sUseImageSize;
     66     static {
     67         // On HC and beyond, assume devices are more capable
     68         if (Build.VERSION.SDK_INT >= 11) {
     69             sUseImageSize = ImageSize.NORMAL;
     70         } else {
     71             if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) {
     72                 // We have plenty of memory; use full sized photos
     73                 sUseImageSize = ImageSize.NORMAL;
     74             } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
     75                 // We have slight less memory; use smaller sized photos
     76                 sUseImageSize = ImageSize.SMALL;
     77             } else {
     78                 // We have little memory; use very small sized photos
     79                 sUseImageSize = ImageSize.EXTRA_SMALL;
     80             }
     81         }
     82     }
     83 
     84     /**
     85      * @return true if the MimeType type is image
     86      */
     87     public static boolean isImageMimeType(String mimeType) {
     88         return mimeType != null && mimeType.startsWith("image/");
     89     }
     90 
     91     /**
     92      * Create a bitmap from a local URI
     93      *
     94      * @param resolver The ContentResolver
     95      * @param uri      The local URI
     96      * @param maxSize  The maximum size (either width or height)
     97      * @return The new bitmap or null
     98      */
     99     public static BitmapResult createLocalBitmap(final ContentResolver resolver, final Uri uri,
    100             final int maxSize) {
    101         final BitmapResult result = new BitmapResult();
    102         final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
    103         try {
    104             final Point bounds = getImageBounds(factory);
    105             if (bounds == null) {
    106                 result.status = BitmapResult.STATUS_EXCEPTION;
    107                 return result;
    108             }
    109 
    110             final BitmapFactory.Options opts = new BitmapFactory.Options();
    111             opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
    112             result.bitmap = decodeStream(factory, null, opts);
    113             result.status = BitmapResult.STATUS_SUCCESS;
    114             return result;
    115 
    116         } catch (FileNotFoundException exception) {
    117             // Do nothing - the photo will appear to be missing
    118         } catch (IOException exception) {
    119             result.status = BitmapResult.STATUS_EXCEPTION;
    120         } catch (IllegalArgumentException exception) {
    121             // Do nothing - the photo will appear to be missing
    122         } catch (SecurityException exception) {
    123             result.status = BitmapResult.STATUS_EXCEPTION;
    124         }
    125         return result;
    126     }
    127 
    128     /**
    129      * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
    130      * BitmapFactory.Options)} that returns {@code null} on {@link
    131      * OutOfMemoryError}.
    132      *
    133      * @param factory    Used to create input streams that holds the raw data to be decoded into a
    134      *                   bitmap.
    135      * @param outPadding If not null, return the padding rect for the bitmap if
    136      *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
    137      *                   no bitmap is returned (null) then padding is
    138      *                   unchanged.
    139      * @param opts       null-ok; Options that control downsampling and whether the
    140      *                   image should be completely decoded, or just is size returned.
    141      * @return The decoded bitmap, or null if the image data could not be
    142      * decoded, or, if opts is non-null, if opts requested only the
    143      * size be returned (in opts.outWidth and opts.outHeight)
    144      */
    145     public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
    146             final BitmapFactory.Options opts) throws FileNotFoundException {
    147         InputStream is = null;
    148         try {
    149             // Determine the orientation for this image
    150             is = factory.createInputStream();
    151             final int orientation = Exif.getOrientation(is, -1);
    152             is.close();
    153 
    154             // Decode the bitmap
    155             is = factory.createInputStream();
    156             final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);
    157 
    158             if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
    159                 Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
    160                         + "Image bytes cannot be decoded into a Bitmap");
    161                 throw new UnsupportedOperationException(
    162                         "Image bytes cannot be decoded into a Bitmap.");
    163             }
    164 
    165             // Rotate the Bitmap based on the orientation
    166             if (originalBitmap != null && orientation != 0) {
    167                 final Matrix matrix = new Matrix();
    168                 matrix.postRotate(orientation);
    169                 return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
    170                         originalBitmap.getHeight(), matrix, true);
    171             }
    172             return originalBitmap;
    173         } catch (OutOfMemoryError oome) {
    174             Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
    175             return null;
    176         } catch (IOException ioe) {
    177             Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
    178             return null;
    179         } finally {
    180             if (is != null) {
    181                 try {
    182                     is.close();
    183                 } catch (IOException e) {
    184                     // Do nothing
    185                 }
    186             }
    187         }
    188     }
    189 
    190     /**
    191      * Gets the image bounds
    192      *
    193      * @param factory Used to create the InputStream.
    194      *
    195      * @return The image bounds
    196      */
    197     private static Point getImageBounds(final InputStreamFactory factory)
    198             throws IOException {
    199         final BitmapFactory.Options opts = new BitmapFactory.Options();
    200         opts.inJustDecodeBounds = true;
    201         decodeStream(factory, null, opts);
    202 
    203         return new Point(opts.outWidth, opts.outHeight);
    204     }
    205 
    206     private static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
    207             final Uri uri) {
    208         final String scheme = uri.getScheme();
    209         if ("http".equals(scheme) || "https".equals(scheme)) {
    210             return new HttpInputStreamFactory(resolver, uri);
    211         } else if ("data".equals(scheme)) {
    212             return new DataInputStreamFactory(resolver, uri);
    213         }
    214         return new BaseInputStreamFactory(resolver, uri);
    215     }
    216 
    217     /**
    218      * Utility class for when an InputStream needs to be read multiple times. For example, one pass
    219      * may load EXIF orientation, and the second pass may do the actual Bitmap decode.
    220      */
    221     public interface InputStreamFactory {
    222 
    223         /**
    224          * Create a new InputStream. The caller of this method must be able to read the input
    225          * stream starting from the beginning.
    226          * @return
    227          */
    228         InputStream createInputStream() throws FileNotFoundException;
    229     }
    230 
    231     private static class BaseInputStreamFactory implements InputStreamFactory {
    232         protected final ContentResolver mResolver;
    233         protected final Uri mUri;
    234 
    235         public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
    236             mResolver = resolver;
    237             mUri = uri;
    238         }
    239 
    240         @Override
    241         public InputStream createInputStream() throws FileNotFoundException {
    242             return mResolver.openInputStream(mUri);
    243         }
    244     }
    245 
    246     private static class DataInputStreamFactory extends BaseInputStreamFactory {
    247         private byte[] mData;
    248 
    249         public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
    250             super(resolver, uri);
    251         }
    252 
    253         @Override
    254         public InputStream createInputStream() throws FileNotFoundException {
    255             if (mData == null) {
    256                 mData = parseDataUri(mUri);
    257                 if (mData == null) {
    258                     return super.createInputStream();
    259                 }
    260             }
    261             return new ByteArrayInputStream(mData);
    262         }
    263 
    264         private byte[] parseDataUri(final Uri uri) {
    265             final String ssp = uri.getSchemeSpecificPart();
    266             try {
    267                 if (ssp.startsWith(BASE64_URI_PREFIX)) {
    268                     final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
    269                     return Base64.decode(base64, Base64.URL_SAFE);
    270                 } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
    271                     final String base64 = ssp.substring(
    272                             ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
    273                     return Base64.decode(base64, Base64.DEFAULT);
    274                 } else {
    275                     return null;
    276                 }
    277             } catch (IllegalArgumentException ex) {
    278                 Log.e(TAG, "Mailformed data URI: " + ex);
    279                 return null;
    280             }
    281         }
    282     }
    283 
    284     private static class HttpInputStreamFactory extends BaseInputStreamFactory {
    285         private byte[] mData;
    286 
    287         public HttpInputStreamFactory(final ContentResolver resolver, final Uri uri) {
    288             super(resolver, uri);
    289         }
    290 
    291         @Override
    292         public InputStream createInputStream() throws FileNotFoundException {
    293             if (mData == null) {
    294                 mData = downloadBytes();
    295                 if (mData == null) {
    296                     return super.createInputStream();
    297                 }
    298             }
    299             return new ByteArrayInputStream(mData);
    300         }
    301 
    302         private byte[] downloadBytes() throws FileNotFoundException {
    303             InputStream is = null;
    304             ByteArrayOutputStream out = null;
    305             try {
    306                 try {
    307                     is = new URL(mUri.toString()).openStream();
    308                 } catch (MalformedURLException e) {
    309                     return null;
    310                 }
    311                 out = new ByteArrayOutputStream();
    312                 final byte[] buffer = new byte[4096];
    313                 int n = is.read(buffer);
    314                 while (n >= 0) {
    315                     out.write(buffer, 0, n);
    316                     n = is.read(buffer);
    317                 }
    318 
    319                 return out.toByteArray();
    320             } catch (IOException ignored) {
    321             } finally {
    322                 if (is != null) {
    323                     try {
    324                         is.close();
    325                     } catch (IOException ignored) {
    326                     }
    327                 }
    328                 if (out != null) {
    329                     try {
    330                         out.close();
    331                     } catch (IOException ignored) {
    332                     }
    333                 }
    334             }
    335             return null;
    336         }
    337     }
    338 }
    339