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
18package com.android.ex.photo.util;
19
20import android.content.ContentResolver;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.Matrix;
24import android.graphics.Point;
25import android.graphics.Rect;
26import android.net.Uri;
27import android.os.Build;
28import android.util.DisplayMetrics;
29import android.util.Log;
30
31import com.android.ex.photo.PhotoViewActivity;
32import com.android.ex.photo.util.Exif;
33
34import java.io.ByteArrayOutputStream;
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.io.InputStream;
38
39/**
40 * Image utilities
41 */
42public class ImageUtils {
43    // Logging
44    private static final String TAG = "ImageUtils";
45
46    /** Minimum class memory class to use full-res photos */
47    private final static long MIN_NORMAL_CLASS = 32;
48    /** Minimum class memory class to use small photos */
49    private final static long MIN_SMALL_CLASS = 24;
50
51    public static enum ImageSize {
52        EXTRA_SMALL,
53        SMALL,
54        NORMAL,
55    }
56
57    public static final ImageSize sUseImageSize;
58    static {
59        // On HC and beyond, assume devices are more capable
60        if (Build.VERSION.SDK_INT >= 11) {
61            sUseImageSize = ImageSize.NORMAL;
62        } else {
63            if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) {
64                // We have plenty of memory; use full sized photos
65                sUseImageSize = ImageSize.NORMAL;
66            } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) {
67                // We have slight less memory; use smaller sized photos
68                sUseImageSize = ImageSize.SMALL;
69            } else {
70                // We have little memory; use very small sized photos
71                sUseImageSize = ImageSize.EXTRA_SMALL;
72            }
73        }
74    }
75
76    /**
77     * @return true if the MimeType type is image
78     */
79    public static boolean isImageMimeType(String mimeType) {
80        return mimeType != null && mimeType.startsWith("image/");
81    }
82
83    /**
84     * Create a bitmap from a local URI
85     *
86     * @param resolver The ContentResolver
87     * @param uri The local URI
88     * @param maxSize The maximum size (either width or height)
89     *
90     * @return The new bitmap or null
91     */
92    public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) {
93        InputStream inputStream = null;
94        try {
95            final BitmapFactory.Options opts = new BitmapFactory.Options();
96            final Point bounds = getImageBounds(resolver, uri);
97
98            inputStream = resolver.openInputStream(uri);
99            opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
100
101            final Bitmap decodedBitmap = decodeStream(inputStream, null, opts);
102
103            // Correct thumbnail orientation as necessary
104            // TODO: Fix rotation if it's actually a problem
105            //return rotateBitmap(resolver, uri, decodedBitmap);
106            return decodedBitmap;
107
108        } catch (FileNotFoundException exception) {
109            // Do nothing - the photo will appear to be missing
110        } catch (IOException exception) {
111            // Do nothing - the photo will appear to be missing
112        } catch (IllegalArgumentException exception) {
113            // Do nothing - the photo will appear to be missing
114        } finally {
115            try {
116                if (inputStream != null) {
117                    inputStream.close();
118                }
119            } catch (IOException ignore) {
120            }
121        }
122        return null;
123    }
124
125    /**
126     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
127     * BitmapFactory.Options)} that returns {@code null} on {@link
128     * OutOfMemoryError}.
129     *
130     * @param is The input stream that holds the raw data to be decoded into a
131     *           bitmap.
132     * @param outPadding If not null, return the padding rect for the bitmap if
133     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
134     *                   no bitmap is returned (null) then padding is
135     *                   unchanged.
136     * @param opts null-ok; Options that control downsampling and whether the
137     *             image should be completely decoded, or just is size returned.
138     * @return The decoded bitmap, or null if the image data could not be
139     *         decoded, or, if opts is non-null, if opts requested only the
140     *         size be returned (in opts.outWidth and opts.outHeight)
141     */
142    public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
143        ByteArrayOutputStream out = null;
144        try {
145            out = new ByteArrayOutputStream();
146            final byte[] buffer = new byte[4096];
147            int n = is.read(buffer);
148            while (n >= 0) {
149                out.write(buffer, 0, n);
150                n = is.read(buffer);
151            }
152            final byte[] bitmapBytes = out.toByteArray();
153
154            // Determine the orientation for this image
155            final int orientation = Exif.getOrientation(bitmapBytes);
156            final Bitmap originalBitmap =
157                    BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length, opts);
158
159            if (originalBitmap != null && orientation != 0) {
160                final Matrix matrix = new Matrix();
161                matrix.postRotate(orientation);
162                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
163                        originalBitmap.getHeight(), matrix, true);
164            }
165            return originalBitmap;
166        } catch (OutOfMemoryError oome) {
167            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
168            return null;
169        } catch (IOException ioe) {
170            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
171            return null;
172        } finally {
173            if (out != null) {
174                try {
175                    out.close();
176                } catch (IOException e) {
177                    // Do nothing
178                }
179            }
180        }
181    }
182
183    /**
184     * Gets the image bounds
185     *
186     * @param resolver The ContentResolver
187     * @param uri The uri
188     *
189     * @return The image bounds
190     */
191    private static Point getImageBounds(ContentResolver resolver, Uri uri)
192            throws IOException {
193        final BitmapFactory.Options opts = new BitmapFactory.Options();
194        InputStream inputStream = null;
195
196        try {
197            opts.inJustDecodeBounds = true;
198            inputStream = resolver.openInputStream(uri);
199            decodeStream(inputStream, null, opts);
200
201            return new Point(opts.outWidth, opts.outHeight);
202        } finally {
203            try {
204                if (inputStream != null) {
205                    inputStream.close();
206                }
207            } catch (IOException ignore) {
208            }
209        }
210    }
211}
212