| /* |
| * Copyright (C) 2011 Google Inc. |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ex.photo.util; |
| |
| import android.content.ContentResolver; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.util.Base64; |
| import android.util.Log; |
| |
| import com.android.ex.photo.PhotoViewActivity; |
| import com.android.ex.photo.util.Exif; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.util.regex.Pattern; |
| |
| |
| /** |
| * Image utilities |
| */ |
| public class ImageUtils { |
| // Logging |
| private static final String TAG = "ImageUtils"; |
| |
| /** Minimum class memory class to use full-res photos */ |
| private final static long MIN_NORMAL_CLASS = 32; |
| /** Minimum class memory class to use small photos */ |
| private final static long MIN_SMALL_CLASS = 24; |
| |
| private static final String BASE64_URI_PREFIX = "base64,"; |
| private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*"); |
| |
| public static enum ImageSize { |
| EXTRA_SMALL, |
| SMALL, |
| NORMAL, |
| } |
| |
| public static final ImageSize sUseImageSize; |
| static { |
| // On HC and beyond, assume devices are more capable |
| if (Build.VERSION.SDK_INT >= 11) { |
| sUseImageSize = ImageSize.NORMAL; |
| } else { |
| if (PhotoViewActivity.sMemoryClass >= MIN_NORMAL_CLASS) { |
| // We have plenty of memory; use full sized photos |
| sUseImageSize = ImageSize.NORMAL; |
| } else if (PhotoViewActivity.sMemoryClass >= MIN_SMALL_CLASS) { |
| // We have slight less memory; use smaller sized photos |
| sUseImageSize = ImageSize.SMALL; |
| } else { |
| // We have little memory; use very small sized photos |
| sUseImageSize = ImageSize.EXTRA_SMALL; |
| } |
| } |
| } |
| |
| /** |
| * @return true if the MimeType type is image |
| */ |
| public static boolean isImageMimeType(String mimeType) { |
| return mimeType != null && mimeType.startsWith("image/"); |
| } |
| |
| /** |
| * Create a bitmap from a local URI |
| * |
| * @param resolver The ContentResolver |
| * @param uri The local URI |
| * @param maxSize The maximum size (either width or height) |
| * |
| * @return The new bitmap or null |
| */ |
| public static Bitmap createLocalBitmap(ContentResolver resolver, Uri uri, int maxSize) { |
| // TODO: make this method not download the image for both getImageBounds and decodeStream |
| InputStream inputStream = null; |
| try { |
| final BitmapFactory.Options opts = new BitmapFactory.Options(); |
| final Point bounds = getImageBounds(resolver, uri); |
| inputStream = openInputStream(resolver, uri); |
| if (bounds == null || inputStream == null) { |
| return null; |
| } |
| opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize); |
| |
| final Bitmap decodedBitmap = decodeStream(inputStream, null, opts); |
| |
| // Correct thumbnail orientation as necessary |
| // TODO: Fix rotation if it's actually a problem |
| //return rotateBitmap(resolver, uri, decodedBitmap); |
| return decodedBitmap; |
| |
| } catch (FileNotFoundException exception) { |
| // Do nothing - the photo will appear to be missing |
| } catch (IOException exception) { |
| // Do nothing - the photo will appear to be missing |
| } catch (IllegalArgumentException exception) { |
| // Do nothing - the photo will appear to be missing |
| } finally { |
| try { |
| if (inputStream != null) { |
| inputStream.close(); |
| } |
| } catch (IOException ignore) { |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect, |
| * BitmapFactory.Options)} that returns {@code null} on {@link |
| * OutOfMemoryError}. |
| * |
| * @param is The input stream that holds the raw data to be decoded into a |
| * bitmap. |
| * @param outPadding If not null, return the padding rect for the bitmap if |
| * it exists, otherwise set padding to [-1,-1,-1,-1]. If |
| * no bitmap is returned (null) then padding is |
| * unchanged. |
| * @param opts null-ok; Options that control downsampling and whether the |
| * image should be completely decoded, or just is size returned. |
| * @return The decoded bitmap, or null if the image data could not be |
| * decoded, or, if opts is non-null, if opts requested only the |
| * size be returned (in opts.outWidth and opts.outHeight) |
| */ |
| public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) { |
| ByteArrayOutputStream out = null; |
| InputStream byteStream = null; |
| try { |
| out = new ByteArrayOutputStream(); |
| final byte[] buffer = new byte[4096]; |
| int n = is.read(buffer); |
| while (n >= 0) { |
| out.write(buffer, 0, n); |
| n = is.read(buffer); |
| } |
| |
| final byte[] bitmapBytes = out.toByteArray(); |
| |
| // Determine the orientation for this image |
| final int orientation = Exif.getOrientation(bitmapBytes); |
| |
| // Create an InputStream from this byte array |
| byteStream = new ByteArrayInputStream(bitmapBytes); |
| |
| final Bitmap originalBitmap = BitmapFactory.decodeStream(byteStream, outPadding, opts); |
| |
| if (originalBitmap != null && orientation != 0) { |
| final Matrix matrix = new Matrix(); |
| matrix.postRotate(orientation); |
| return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), |
| originalBitmap.getHeight(), matrix, true); |
| } |
| return originalBitmap; |
| } catch (OutOfMemoryError oome) { |
| Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome); |
| return null; |
| } catch (IOException ioe) { |
| Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe); |
| return null; |
| } finally { |
| if (out != null) { |
| try { |
| out.close(); |
| } catch (IOException e) { |
| // Do nothing |
| } |
| } |
| if (byteStream != null) { |
| try { |
| byteStream.close(); |
| } catch (IOException e) { |
| // Do nothing |
| } |
| } |
| } |
| } |
| |
| /** |
| * Gets the image bounds |
| * |
| * @param resolver The ContentResolver |
| * @param uri The uri |
| * |
| * @return The image bounds |
| */ |
| private static Point getImageBounds(ContentResolver resolver, Uri uri) |
| throws IOException { |
| final BitmapFactory.Options opts = new BitmapFactory.Options(); |
| InputStream inputStream = null; |
| String scheme = uri.getScheme(); |
| try { |
| opts.inJustDecodeBounds = true; |
| inputStream = openInputStream(resolver, uri); |
| if (inputStream == null) { |
| return null; |
| } |
| decodeStream(inputStream, null, opts); |
| |
| return new Point(opts.outWidth, opts.outHeight); |
| } finally { |
| try { |
| if (inputStream != null) { |
| inputStream.close(); |
| } |
| } catch (IOException ignore) { |
| } |
| } |
| } |
| |
| private static InputStream openInputStream(ContentResolver resolver, Uri uri) throws |
| FileNotFoundException { |
| String scheme = uri.getScheme(); |
| if ("http".equals(scheme) || "https".equals(scheme)) { |
| try { |
| return new URL(uri.toString()).openStream(); |
| } catch (MalformedURLException e) { |
| // Fall-back to the previous behaviour, just in case |
| Log.w(TAG, "Could not convert the uri to url: " + uri.toString()); |
| return resolver.openInputStream(uri); |
| } catch (IOException e) { |
| Log.w(TAG, "Could not open input stream for uri: " + uri.toString()); |
| return null; |
| } |
| } else if ("data".equals(scheme)) { |
| byte[] data = parseDataUri(uri); |
| if (data != null) { |
| return new ByteArrayInputStream(data); |
| } |
| } |
| return resolver.openInputStream(uri); |
| } |
| |
| private static byte[] parseDataUri(Uri uri) { |
| String ssp = uri.getSchemeSpecificPart(); |
| try { |
| if (ssp.startsWith(BASE64_URI_PREFIX)) { |
| String base64 = ssp.substring(BASE64_URI_PREFIX.length()); |
| return Base64.decode(base64, Base64.URL_SAFE); |
| } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){ |
| String base64 = ssp.substring( |
| ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length()); |
| return Base64.decode(base64, Base64.DEFAULT); |
| } else { |
| return null; |
| } |
| } catch (IllegalArgumentException ex) { |
| Log.e(TAG, "Mailformed data URI: " + ex); |
| return null; |
| } |
| } |
| } |