From 0dca1b590f4cae2fe42afc9245f1a8ea81f9e6d3 Mon Sep 17 00:00:00 2001 From: Ruben Brunk Date: Fri, 12 Jul 2013 17:23:39 -0700 Subject: Combine ImageLoader and CropLoader utility classes. Bug: 9356969 Bug: 9170644 - Refactor to eliminate code duplication in image loading classes. - Fix bugs caused by the inability to determine MIME types for images that aren't in Gallery's content provider (causes b/9356969). Change-Id: I30c2ba5c0cd37aac624681a2cec9abddaa78f9c2 --- .../gallery3d/filtershow/FilterShowActivity.java | 20 +- .../gallery3d/filtershow/cache/ImageLoader.java | 363 ++++++++++++--------- .../gallery3d/filtershow/crop/CropActivity.java | 8 +- .../gallery3d/filtershow/crop/CropLoader.java | 318 ------------------ .../filtershow/filters/ImageFilterDraw.java | 7 +- .../gallery3d/filtershow/imageshow/ImageShow.java | 3 +- .../filtershow/imageshow/MasterImage.java | 28 +- .../gallery3d/filtershow/tools/SaveCopyTask.java | 62 +++- 8 files changed, 291 insertions(+), 518 deletions(-) delete mode 100644 src/com/android/gallery3d/filtershow/crop/CropLoader.java (limited to 'src') diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java index c29709b91..bee764b25 100644 --- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java +++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java @@ -506,9 +506,11 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL if (highresPreviewSize > originalBounds.width()) { highresPreviewSize = originalBounds.width(); } - Bitmap originalHires = ImageLoader.loadOrientedScaledBitmap(master, - master.getActivity(), master.getUri(), highresPreviewSize, false, - master.getOrientation()); + Rect bounds = new Rect(); + Bitmap originalHires = ImageLoader.loadOrientedConstrainedBitmap(master.getUri(), + master.getActivity(), highresPreviewSize, + master.getOrientation(), bounds); + master.setOriginalBounds(bounds); master.setOriginalBitmapHighres(originalHires); master.warnListeners(); } @@ -639,14 +641,12 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL super.onDestroy(); } + // TODO: find a more robust way of handling image size selection + // for high screen densities. private int getScreenImageSize() { - DisplayMetrics metrics = new DisplayMetrics(); - Display display = getWindowManager().getDefaultDisplay(); - Point size = new Point(); - display.getSize(size); - display.getMetrics(metrics); - int msize = Math.min(size.x, size.y); - return (133 * msize) / metrics.densityDpi; + DisplayMetrics outMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(outMetrics); + return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels); } private void showSavingProgress(String albumName) { diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java index ca8c7a392..7c594c67f 100644 --- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java +++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java @@ -29,18 +29,15 @@ import android.graphics.Rect; import android.net.Uri; import android.provider.MediaStore; import android.util.Log; +import android.webkit.MimeTypeMap; import com.adobe.xmp.XMPException; import com.adobe.xmp.XMPMeta; import com.android.gallery3d.common.Utils; import com.android.gallery3d.exif.ExifInterface; -import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.imageshow.MasterImage; -import com.android.gallery3d.filtershow.pipeline.ImagePreset; -import com.android.gallery3d.filtershow.tools.SaveCopyTask; import com.android.gallery3d.util.XmpUtilHelper; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -50,8 +47,6 @@ public final class ImageLoader { private static final String LOGTAG = "ImageLoader"; public static final String JPEG_MIME_TYPE = "image/jpeg"; - - public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; public static final int DEFAULT_COMPRESS_QUALITY = 95; public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; @@ -65,42 +60,39 @@ public final class ImageLoader { private static final int BITMAP_LOAD_BACKOUT_ATTEMPTS = 5; - public static final int MAX_BITMAP_DIM = 900; - public static final int SMALL_BITMAP_DIM = 160; - private ImageLoader() {} - public static int getOrientation(Context context, Uri uri) { - if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - String mimeType = context.getContentResolver().getType(uri); - if (mimeType != ImageLoader.JPEG_MIME_TYPE) { - return -1; - } - String path = uri.getPath(); - int orientation = -1; - ExifInterface exif = new ExifInterface(); - try { - exif.readExif(path); - orientation = ExifInterface.getRotationForOrientationValue( - exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue()); - } catch (IOException e) { - Log.w(LOGTAG, "Failed to read EXIF orientation", e); - } - return orientation; + /** + * Returns the Mime type for a Url. Safe to use with Urls that do not + * come from Gallery's content provider. + */ + public static String getMimeType(Uri src) { + String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString()); + String ret = null; + if (postfix != null) { + ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix); } + return ret; + } + + /** + * Returns the image's orientation flag. Defaults to ORI_NORMAL if no valid + * orientation was found. + */ + public static int getMetadataOrientation(Context context, Uri uri) { + if (uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getOrientation"); + } + + // First try to find orientation data in Gallery's ContentProvider. Cursor cursor = null; try { cursor = context.getContentResolver().query(uri, - new String[] { - MediaStore.Images.ImageColumns.ORIENTATION - }, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null); - if (cursor.moveToNext()) { + if (cursor != null && cursor.moveToNext()) { int ori = cursor.getInt(0); - switch (ori) { - case 0: - return ORI_NORMAL; case 90: return ORI_ROTATE_90; case 270: @@ -108,24 +100,73 @@ public final class ImageLoader { case 180: return ORI_ROTATE_180; default: - return -1; + return ORI_NORMAL; } - } else { - return -1; } } catch (SQLiteException e) { - return -1; + // Do nothing } catch (IllegalArgumentException e) { - return -1; + // Do nothing } finally { Utils.closeSilently(cursor); } + + // Fall back to checking EXIF tags in file. + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + String mimeType = getMimeType(uri); + if (!JPEG_MIME_TYPE.equals(mimeType)) { + return ORI_NORMAL; + } + String path = uri.getPath(); + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(path); + Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); + if (tagval != null) { + int orientation = tagval; + switch(orientation) { + case ORI_NORMAL: + case ORI_ROTATE_90: + case ORI_ROTATE_180: + case ORI_ROTATE_270: + case ORI_FLIP_HOR: + case ORI_FLIP_VERT: + case ORI_TRANSPOSE: + case ORI_TRANSVERSE: + return orientation; + default: + return ORI_NORMAL; + } + } + } catch (IOException e) { + Log.w(LOGTAG, "Failed to read EXIF orientation", e); + } + } + return ORI_NORMAL; } - public static Bitmap decodeImage(Context context, int id, BitmapFactory.Options options) { - return BitmapFactory.decodeResource(context.getResources(), id, options); + /** + * Returns the rotation of image at the given URI as one of 0, 90, 180, + * 270. Defaults to 0. + */ + public static int getMetadataRotation(Context context, Uri uri) { + int orientation = getMetadataOrientation(context, uri); + switch(orientation) { + case ORI_ROTATE_90: + return 90; + case ORI_ROTATE_180: + return 180; + case ORI_ROTATE_270: + return 270; + default: + return 0; + } } + /** + * Takes an orientation and a bitmap, and returns the bitmap transformed + * to that orientation. + */ public static Bitmap orientBitmap(Bitmap bitmap, int ori) { Matrix matrix = new Matrix(); int w = bitmap.getWidth(); @@ -166,12 +207,16 @@ public final class ImageLoader { default: return bitmap; } - return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } - private static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options, + /** + * Returns the bitmap for the rectangular region given by "bounds" + * if it is a subset of the bitmap stored at uri. Otherwise returns + * null. + */ + public static Bitmap loadRegionBitmap(Context context, Uri uri, BitmapFactory.Options options, Rect bounds) { InputStream is = null; try { @@ -184,75 +229,120 @@ public final class ImageLoader { } return decoder.decodeRegion(bounds, options); } catch (FileNotFoundException e) { - Log.e(LOGTAG, "FileNotFoundException: " + uri); - } catch (Exception e) { - e.printStackTrace(); + Log.e(LOGTAG, "FileNotFoundException for " + uri, e); + } catch (IOException e) { + Log.e(LOGTAG, "FileNotFoundException for " + uri, e); } finally { Utils.closeSilently(is); } return null; } - public static Bitmap loadOrientedScaledBitmap(MasterImage master, Context context, Uri uri, - int size, boolean enforceSize, int orientation) { - Bitmap bmap = loadScaledBitmap(master, context, uri, size, enforceSize); - if (bmap != null) { - bmap = orientBitmap(bmap, orientation); - } - return bmap; + /** + * Returns the bounds of the bitmap stored at a given Url. + */ + public static Rect loadBitmapBounds(Context context, Uri uri) { + BitmapFactory.Options o = new BitmapFactory.Options(); + loadBitmap(context, uri, o); + return new Rect(0, 0, o.outWidth, o.outHeight); } - public static Bitmap loadScaledBitmap(MasterImage master, Context context, Uri uri, int size, - boolean enforceSize) { - InputStream is = null; - try { - is = context.getContentResolver().openInputStream(uri); - Log.v(LOGTAG, "loading uri " + uri.getPath() + " input stream: " - + is); - BitmapFactory.Options o = new BitmapFactory.Options(); - o.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, o); - - int width_tmp = o.outWidth; - int height_tmp = o.outHeight; - - master.setOriginalBounds(new Rect(0, 0, width_tmp, height_tmp)); - - int scale = 1; - while (true) { - if (width_tmp <= 2 || height_tmp <= 2) { - break; - } - if (!enforceSize - || (width_tmp <= MAX_BITMAP_DIM - && height_tmp <= MAX_BITMAP_DIM)) { - if (width_tmp / 2 < size || height_tmp / 2 < size) { - break; - } - } - width_tmp /= 2; - height_tmp /= 2; - scale *= 2; - } + /** + * Loads a bitmap that has been downsampled using sampleSize from a given url. + */ + public static Bitmap loadDownsampledBitmap(Context context, Uri uri, int sampleSize) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + options.inSampleSize = sampleSize; + return loadBitmap(context, uri, options); + } - // decode with inSampleSize - BitmapFactory.Options o2 = new BitmapFactory.Options(); - o2.inSampleSize = scale; - o2.inMutable = true; - Utils.closeSilently(is); + /** + * Returns the bitmap from the given uri loaded using the given options. + * Returns null on failure. + */ + public static Bitmap loadBitmap(Context context, Uri uri, BitmapFactory.Options o) { + if (uri == null || context == null) { + throw new IllegalArgumentException("bad argument to loadBitmap"); + } + InputStream is = null; + try { is = context.getContentResolver().openInputStream(uri); - return BitmapFactory.decodeStream(is, null, o2); + return BitmapFactory.decodeStream(is, null, o); } catch (FileNotFoundException e) { - Log.e(LOGTAG, "FileNotFoundException: " + uri); - } catch (Exception e) { - e.printStackTrace(); + Log.e(LOGTAG, "FileNotFoundException for " + uri, e); } finally { Utils.closeSilently(is); } return null; } + /** + * Loads a bitmap at a given URI that is downsampled so that both sides are + * smaller than maxSideLength. The Bitmap's original dimensions are stored + * in the rect originalBounds. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @param maxSideLength max side length of returned bitmap. + * @param originalBounds set to the actual bounds of the stored bitmap. + * @return downsampled bitmap or null if this operation failed. + */ + public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, + Rect originalBounds) { + if (maxSideLength <= 0 || originalBounds == null || uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + // Get width and height of stored bitmap + Rect storedBounds = loadBitmapBounds(context, uri); + originalBounds.set(storedBounds); + int w = storedBounds.width(); + int h = storedBounds.height(); + + // If bitmap cannot be decoded, return null + if (w <= 0 || h <= 0) { + return null; + } + + // Find best downsampling size + int imageSide = Math.max(w, h); + int sampleSize = 1; + while (imageSide > maxSideLength) { + imageSide >>>= 1; + sampleSize <<= 1; + } + + // Make sure sample size is reasonable + if (sampleSize <= 0 || + 0 >= (int) (Math.min(w, h) / sampleSize)) { + return null; + } + return loadDownsampledBitmap(context, uri, sampleSize); + } + + /** + * Loads a bitmap at a given URI that is downsampled so that both sides are + * smaller than maxSideLength. The Bitmap's original dimensions are stored + * in the rect originalBounds. The output is also transformed to the given + * orientation. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @param maxSideLength max side length of returned bitmap. + * @param orientation the orientation to transform the bitmap to. + * @param originalBounds set to the actual bounds of the stored bitmap. + * @return downsampled bitmap or null if this operation failed. + */ + public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, + int orientation, Rect originalBounds) { + Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds); + if (bmap != null) { + bmap = orientBitmap(bmap, orientation); + } + return bmap; + } + public static Bitmap getScaleOneImageForPreset(Context context, Uri uri, Rect bounds, Rect destination) { BitmapFactory.Options options = new BitmapFactory.Options(); @@ -272,88 +362,53 @@ public final class ImageLoader { return bmp; } - public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, - File destination) { - Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); - new SaveCopyTask(filterShowActivity, MasterImage.getImage().getUri(), selectedImageUri, - destination, - new SaveCopyTask.Callback() { - - @Override - public void onComplete(Uri result) { - filterShowActivity.completeSaveImage(result); - } - - }).execute(preset); - } - - public static Bitmap loadMutableBitmap(Context context, Uri sourceUri) { - BitmapFactory.Options options = new BitmapFactory.Options(); - return loadMutableBitmap(context, sourceUri, options); - } - - public static Bitmap loadMutableBitmap(Context context, Uri sourceUri, - BitmapFactory.Options options) { - // TODO: on <3.x we need a copy of the bitmap (inMutable doesn't - // exist) - options.inMutable = true; - - Bitmap bitmap = decodeUriWithBackouts(context, sourceUri, options); - if (bitmap == null) { - return null; - } - int orientation = ImageLoader.getOrientation(context, sourceUri); - bitmap = ImageLoader.orientBitmap(bitmap, orientation); - return bitmap; - } - - public static Bitmap decodeUriWithBackouts(Context context, Uri sourceUri, - BitmapFactory.Options options) { + /** + * Loads a bitmap that is downsampled by at least the input sample size. In + * low-memory situations, the bitmap may be downsampled further. + */ + public static Bitmap loadBitmapWithBackouts(Context context, Uri sourceUri, int sampleSize) { boolean noBitmap = true; int num_tries = 0; - InputStream is = getInputStream(context, sourceUri); - - if (options.inSampleSize < 1) { - options.inSampleSize = 1; + if (sampleSize <= 0) { + sampleSize = 1; } - // Stopgap fix for low-memory devices. Bitmap bmap = null; while (noBitmap) { - if (is == null) { - return null; - } try { // Try to decode, downsample if low-memory. - bmap = BitmapFactory.decodeStream(is, null, options); + bmap = loadDownsampledBitmap(context, sourceUri, sampleSize); noBitmap = false; } catch (java.lang.OutOfMemoryError e) { - // Try 5 times before failing for good. + // Try with more downsampling before failing for good. if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { throw e; } - is = null; bmap = null; System.gc(); - is = getInputStream(context, sourceUri); - options.inSampleSize *= 2; + sampleSize *= 2; } } - Utils.closeSilently(is); return bmap; } - private static InputStream getInputStream(Context context, Uri sourceUri) { - InputStream is = null; - try { - is = context.getContentResolver().openInputStream(sourceUri); - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "could not load bitmap ", e); - Utils.closeSilently(is); - is = null; + /** + * Loads an oriented bitmap that is downsampled by at least the input sample + * size. In low-memory situations, the bitmap may be downsampled further. + */ + public static Bitmap loadOrientedBitmapWithBackouts(Context context, Uri sourceUri, + int sampleSize) { + Bitmap bitmap = loadBitmapWithBackouts(context, sourceUri, sampleSize); + if (bitmap == null) { + return null; } - return is; + int orientation = getMetadataOrientation(context, sourceUri); + bitmap = orientBitmap(bitmap, orientation); + return bitmap; } + /** + * Loads bitmap from a resource that may be downsampled in low-memory situations. + */ public static Bitmap decodeResourceWithBackouts(Resources res, BitmapFactory.Options options, int id) { boolean noBitmap = true; @@ -370,7 +425,7 @@ public final class ImageLoader { res, id, options); noBitmap = false; } catch (java.lang.OutOfMemoryError e) { - // Try 5 times before failing for good. + // Retry before failing for good. if (++num_tries >= BITMAP_LOAD_BACKOUT_ATTEMPTS) { throw e; } diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java index d349d5d67..d14c090fa 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropActivity.java +++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java @@ -44,6 +44,8 @@ import android.widget.Toast; import com.android.gallery3d.R; import com.android.gallery3d.common.Utils; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.tools.SaveCopyTask; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -261,9 +263,9 @@ public class CropActivity extends Activity { @Override protected Bitmap doInBackground(Uri... params) { Uri uri = params[0]; - Bitmap bmap = CropLoader.getConstrainedBitmap(uri, mContext, mBitmapSize, + Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize, mOriginalBounds); - mOrientation = CropLoader.getMetadataRotation(uri, mContext); + mOrientation = ImageLoader.getMetadataRotation(mContext, uri); return bmap; } @@ -297,7 +299,7 @@ public class CropActivity extends Activity { } } if (flags == 0) { - destinationUri = CropLoader.makeAndInsertUri(this, mSourceUri); + destinationUri = SaveCopyTask.makeAndInsertUri(this, mSourceUri); if (destinationUri != null) { flags |= DO_EXTRA_OUTPUT; } diff --git a/src/com/android/gallery3d/filtershow/crop/CropLoader.java b/src/com/android/gallery3d/filtershow/crop/CropLoader.java deleted file mode 100644 index 53a9ebc14..000000000 --- a/src/com/android/gallery3d/filtershow/crop/CropLoader.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright (C) 2013 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.gallery3d.filtershow.crop; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteException; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Rect; -import android.net.Uri; -import android.os.Environment; -import android.provider.MediaStore; -import android.provider.MediaStore.Images; -import android.provider.MediaStore.Images.ImageColumns; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifInterface; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.sql.Date; -import java.text.SimpleDateFormat; - -/** - * This class contains static methods for loading a bitmap and - * maintains no instance state. - */ -public abstract class CropLoader { - public static final String LOGTAG = "CropLoader"; - public static final String JPEG_MIME_TYPE = "image/jpeg"; - - private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss"; - public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; - - /** - * Returns the orientation of image at the given URI as one of 0, 90, 180, - * 270. - * - * @param uri URI of image to open. - * @param context context whose ContentResolver to use. - * @return the orientation of the image. Defaults to 0. - */ - public static int getMetadataRotation(Uri uri, Context context) { - if (uri == null || context == null) { - throw new IllegalArgumentException("bad argument to getScaledBitmap"); - } - - // First try to find orientation data in Gallery's ContentProvider. - Cursor cursor = null; - try { - cursor = context.getContentResolver().query(uri, - new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, - null, null, null); - if (cursor != null && cursor.moveToNext()) { - int ori = cursor.getInt(0); - return (ori < 0) ? 0 : ori; - } - } catch (SQLiteException e) { - // Do nothing - } catch (IllegalArgumentException e) { - // Do nothing - } finally { - Utils.closeSilently(cursor); - } - - // Fall back to checking EXIF tags in file. - if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - String mimeType = getMimeType(uri); - if (!JPEG_MIME_TYPE.equals(mimeType)) { - return 0; - } - String path = uri.getPath(); - int orientation = 0; - ExifInterface exif = new ExifInterface(); - try { - exif.readExif(path); - Integer tagval = exif.getTagIntValue(ExifInterface.TAG_ORIENTATION); - if (tagval != null) { - orientation = ExifInterface.getRotationForOrientationValue(tagval.shortValue()); - } - } catch (IOException e) { - Log.w(LOGTAG, "Failed to read EXIF orientation", e); - } - return orientation; - } - return 0; - } - - /** - * Gets a bitmap at a given URI that is downsampled so that both sides are - * smaller than maxSideLength. The Bitmap's original dimensions are stored - * in the rect originalBounds. - * - * @param uri URI of image to open. - * @param context context whose ContentResolver to use. - * @param maxSideLength max side length of returned bitmap. - * @param originalBounds set to the actual bounds of the stored bitmap. - * @return downsampled bitmap or null if this operation failed. - */ - public static Bitmap getConstrainedBitmap(Uri uri, Context context, int maxSideLength, - Rect originalBounds) { - if (maxSideLength <= 0 || originalBounds == null || uri == null || context == null) { - throw new IllegalArgumentException("bad argument to getScaledBitmap"); - } - InputStream is = null; - try { - // Get width and height of stored bitmap - is = context.getContentResolver().openInputStream(uri); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, options); - int w = options.outWidth; - int h = options.outHeight; - originalBounds.set(0, 0, w, h); - - // If bitmap cannot be decoded, return null - if (w <= 0 || h <= 0) { - return null; - } - - options = new BitmapFactory.Options(); - - // Find best downsampling size - int imageSide = Math.max(w, h); - options.inSampleSize = 1; - if (imageSide > maxSideLength) { - int shifts = 1 + Integer.numberOfLeadingZeros(maxSideLength) - - Integer.numberOfLeadingZeros(imageSide); - options.inSampleSize <<= shifts; - } - - // Make sure sample size is reasonable - if (options.inSampleSize <= 0 || - 0 >= (int) (Math.min(w, h) / options.inSampleSize)) { - return null; - } - - // Decode actual bitmap. - options.inMutable = true; - is.close(); - is = context.getContentResolver().openInputStream(uri); - return BitmapFactory.decodeStream(is, null, options); - } catch (FileNotFoundException e) { - Log.e(LOGTAG, "FileNotFoundException: " + uri, e); - } catch (IOException e) { - Log.e(LOGTAG, "IOException: " + uri, e); - } finally { - Utils.closeSilently(is); - } - return null; - } - - /** - * Gets a bitmap that has been downsampled using sampleSize. - * - * @param uri URI of image to open. - * @param context context whose ContentResolver to use. - * @param sampleSize downsampling amount. - * @return downsampled bitmap. - */ - public static Bitmap getBitmap(Uri uri, Context context, int sampleSize) { - if (uri == null || context == null) { - throw new IllegalArgumentException("bad argument to getScaledBitmap"); - } - InputStream is = null; - try { - is = context.getContentResolver().openInputStream(uri); - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inMutable = true; - options.inSampleSize = sampleSize; - return BitmapFactory.decodeStream(is, null, options); - } catch (FileNotFoundException e) { - Log.e(LOGTAG, "FileNotFoundException: " + uri, e); - } finally { - Utils.closeSilently(is); - } - return null; - } - - // TODO: Super gnarly (copied from SaveCopyTask.java), do cleanup. - - public static File getFinalSaveDirectory(Context context, Uri sourceUri) { - File saveDirectory = getSaveDirectory(context, sourceUri); - if ((saveDirectory == null) || !saveDirectory.canWrite()) { - saveDirectory = new File(Environment.getExternalStorageDirectory(), - DEFAULT_SAVE_DIRECTORY); - } - // Create the directory if it doesn't exist - if (!saveDirectory.exists()) - saveDirectory.mkdirs(); - return saveDirectory; - } - - - - public static String getNewFileName(long time) { - return new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); - } - - public static File getNewFile(Context context, Uri sourceUri, String filename) { - File saveDirectory = getFinalSaveDirectory(context, sourceUri); - return new File(saveDirectory, filename + ".JPG"); - } - - private interface ContentResolverQueryCallback { - - void onCursorResult(Cursor cursor); - } - - private static void querySource(Context context, Uri sourceUri, String[] projection, - ContentResolverQueryCallback callback) { - ContentResolver contentResolver = context.getContentResolver(); - Cursor cursor = null; - try { - cursor = contentResolver.query(sourceUri, projection, null, null, - null); - if ((cursor != null) && cursor.moveToNext()) { - callback.onCursorResult(cursor); - } - } catch (Exception e) { - // Ignore error for lacking the data column from the source. - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - private static File getSaveDirectory(Context context, Uri sourceUri) { - final File[] dir = new File[1]; - querySource(context, sourceUri, new String[] { - ImageColumns.DATA }, new ContentResolverQueryCallback() { - @Override - public void onCursorResult(Cursor cursor) { - dir[0] = new File(cursor.getString(0)).getParentFile(); - } - }); - return dir[0]; - } - - private static String getMimeType(Uri src) { - String postfix = MimeTypeMap.getFileExtensionFromUrl(src.toString()); - String ret = null; - if (postfix != null) { - ret = MimeTypeMap.getSingleton().getMimeTypeFromExtension(postfix); - } - return ret; - } - - public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName, - long time) { - time /= 1000; - - final ContentValues values = new ContentValues(); - values.put(Images.Media.TITLE, saveFileName); - values.put(Images.Media.DISPLAY_NAME, file.getName()); - values.put(Images.Media.MIME_TYPE, "image/jpeg"); - values.put(Images.Media.DATE_TAKEN, time); - values.put(Images.Media.DATE_MODIFIED, time); - values.put(Images.Media.DATE_ADDED, time); - values.put(Images.Media.ORIENTATION, 0); - values.put(Images.Media.DATA, file.getAbsolutePath()); - values.put(Images.Media.SIZE, file.length()); - - final String[] projection = new String[] { - ImageColumns.DATE_TAKEN, - ImageColumns.LATITUDE, ImageColumns.LONGITUDE, - }; - querySource(context, sourceUri, projection, - new ContentResolverQueryCallback() { - - @Override - public void onCursorResult(Cursor cursor) { - values.put(Images.Media.DATE_TAKEN, cursor.getLong(0)); - - double latitude = cursor.getDouble(1); - double longitude = cursor.getDouble(2); - // TODO: Change || to && after the default location - // issue is fixed. - if ((latitude != 0f) || (longitude != 0f)) { - values.put(Images.Media.LATITUDE, latitude); - values.put(Images.Media.LONGITUDE, longitude); - } - } - }); - - return context.getContentResolver().insert( - Images.Media.EXTERNAL_CONTENT_URI, values); - } - - public static Uri makeAndInsertUri(Context context, Uri sourceUri) { - long time = System.currentTimeMillis(); - String filename = getNewFileName(time); - File file = getNewFile(context, sourceUri, filename); - return insertContent(context, sourceUri, file, filename, time); - } -} diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java index 0458a2221..7df5ffb64 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterDraw.java @@ -133,15 +133,16 @@ public class ImageFilterDraw extends ImageFilter { if (mBrush == null) { BitmapFactory.Options opt = new BitmapFactory.Options(); opt.inPreferredConfig = Bitmap.Config.ALPHA_8; - mBrush = ImageLoader.decodeImage(MasterImage.getImage().getActivity(), mBrushID, - opt); + mBrush = BitmapFactory.decodeResource(MasterImage.getImage().getActivity() + .getResources(), mBrushID, opt); mBrush = mBrush.extractAlpha(); } return mBrush; } @Override - public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, Matrix toScrMatrix, + public void paint(FilterDrawRepresentation.StrokeData sd, Canvas canvas, + Matrix toScrMatrix, int quality) { if (sd == null || sd.mPath == null) { return; diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java index 8b9d57cb9..2da358c2c 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java @@ -40,6 +40,7 @@ import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.filters.ImageFilter; import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.tools.SaveCopyTask; import java.io.File; @@ -394,7 +395,7 @@ public class ImageShow extends View implements OnGestureListener, } public void saveImage(FilterShowActivity filterShowActivity, File file) { - ImageLoader.saveImage(getImagePreset(), filterShowActivity, file); + SaveCopyTask.saveImage(getImagePreset(), filterShowActivity, file); } diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java index 7b11cf35e..08d8c45eb 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java +++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java @@ -26,29 +26,29 @@ import android.os.Handler; import android.os.Message; import com.android.gallery3d.filtershow.FilterShowActivity; -import com.android.gallery3d.filtershow.history.HistoryManager; -import com.android.gallery3d.filtershow.history.HistoryItem; -import com.android.gallery3d.filtershow.pipeline.FilteringPipeline; import com.android.gallery3d.filtershow.cache.ImageLoader; -import com.android.gallery3d.filtershow.pipeline.RenderingRequest; -import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; import com.android.gallery3d.filtershow.filters.FilterRepresentation; import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.history.HistoryItem; +import com.android.gallery3d.filtershow.history.HistoryManager; import com.android.gallery3d.filtershow.pipeline.Buffer; +import com.android.gallery3d.filtershow.pipeline.FilteringPipeline; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.RenderingRequest; +import com.android.gallery3d.filtershow.pipeline.RenderingRequestCaller; import com.android.gallery3d.filtershow.pipeline.SharedBuffer; import com.android.gallery3d.filtershow.pipeline.SharedPreset; -import com.android.gallery3d.filtershow.pipeline.ImagePreset; import com.android.gallery3d.filtershow.state.StateAdapter; import java.util.Vector; public class MasterImage implements RenderingRequestCaller { - - private static final String LOGTAG = "MasterImage"; private boolean DEBUG = false; private static final boolean DISABLEZOOM = false; + public static final int SMALL_BITMAP_DIM = 160; + public static final int MAX_BITMAP_DIM = 900; private static MasterImage sMasterImage = null; private boolean mSupportsHighRes = false; @@ -188,13 +188,16 @@ public class MasterImage implements RenderingRequestCaller { public boolean loadBitmap(Uri uri, int size) { setUri(uri); - mOrientation = ImageLoader.getOrientation(mActivity, uri); - mOriginalBitmapLarge = ImageLoader.loadOrientedScaledBitmap(this, mActivity, uri, size, - true, mOrientation); + mOrientation = ImageLoader.getMetadataOrientation(mActivity, uri); + Rect originalBounds = new Rect(); + mOriginalBitmapLarge = ImageLoader.loadOrientedConstrainedBitmap(uri, mActivity, + Math.min(MAX_BITMAP_DIM, size), + mOrientation, originalBounds); + setOriginalBounds(originalBounds); if (mOriginalBitmapLarge == null) { return false; } - int sw = ImageLoader.SMALL_BITMAP_DIM; + int sw = SMALL_BITMAP_DIM; int sh = (int) (sw * (float) mOriginalBitmapLarge.getHeight() / (float) mOriginalBitmapLarge .getWidth()); mOriginalBitmapSmall = Bitmap.createScaledBitmap(mOriginalBitmapLarge, sw, sh, true); @@ -547,7 +550,6 @@ public class MasterImage implements RenderingRequestCaller { } } - public float getScaleFactor() { return mScaleFactor; } diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java index 6c1b2d017..dcf0ae166 100644 --- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java +++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java @@ -28,13 +28,16 @@ import android.os.Environment; import android.provider.MediaStore; import android.provider.MediaStore.Images; import android.provider.MediaStore.Images.ImageColumns; +import android.provider.MediaStore.Images.Media; import android.util.Log; import com.android.gallery3d.common.Utils; import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.pipeline.CachingPipeline; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.imageshow.MasterImage; import com.android.gallery3d.filtershow.pipeline.ImagePreset; import com.android.gallery3d.util.UsageStatistics; import com.android.gallery3d.util.XmpUtilHelper; @@ -59,12 +62,10 @@ public class SaveCopyTask extends AsyncTask { * Callback for the completed asynchronous task. */ public interface Callback { - void onComplete(Uri result); } public interface ContentResolverQueryCallback { - void onCursorResult(Cursor cursor); } @@ -84,6 +85,7 @@ public class SaveCopyTask extends AsyncTask { private final Callback mCallback; private final File mDestinationFile; private final Uri mSelectedImageUri; + public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; // In order to support the new edit-save behavior such that user won't see // the edited image together with the original image, we are adding a new @@ -142,10 +144,10 @@ public class SaveCopyTask extends AsyncTask { } public static File getFinalSaveDirectory(Context context, Uri sourceUri) { - File saveDirectory = getSaveDirectory(context, sourceUri); + File saveDirectory = SaveCopyTask.getSaveDirectory(context, sourceUri); if ((saveDirectory == null) || !saveDirectory.canWrite()) { saveDirectory = new File(Environment.getExternalStorageDirectory(), - ImageLoader.DEFAULT_SAVE_DIRECTORY); + SaveCopyTask.DEFAULT_SAVE_DIRECTORY); } // Create the directory if it doesn't exist if (!saveDirectory.exists()) @@ -241,6 +243,9 @@ public class SaveCopyTask extends AsyncTask { public ExifInterface getExifData(Uri source) { ExifInterface exif = new ExifInterface(); String mimeType = mContext.getContentResolver().getType(mSelectedImageUri); + if (mimeType == null) { + mimeType = ImageLoader.getMimeType(mSelectedImageUri); + } if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) { InputStream inStream = null; try { @@ -293,16 +298,16 @@ public class SaveCopyTask extends AsyncTask { // create a local copy as usual. if (srcFile != null) { srcFile.renameTo(mDestinationFile); - uri = insertContent(mContext, mSelectedImageUri, mDestinationFile, + uri = SaveCopyTask.insertContent(mContext, mSelectedImageUri, mDestinationFile, System.currentTimeMillis()); removeSelectedImage(); return uri; } } - BitmapFactory.Options options = new BitmapFactory.Options(); boolean noBitmap = true; int num_tries = 0; + int sampleSize = 1; // If necessary, move the source file into the auxiliary directory, // newSourceUri is then pointing to the new location. @@ -317,11 +322,13 @@ public class SaveCopyTask extends AsyncTask { while (noBitmap) { try { // Try to do bitmap operations, downsample if low-memory - Bitmap bitmap = ImageLoader.loadMutableBitmap(mContext, newSourceUri, options); + Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri, + sampleSize); if (bitmap == null) { return null; } - CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Saving"); + CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), + "Saving"); bitmap = pipeline.renderFinalImage(bitmap, preset); Object xmp = getPanoramaXMPData(mSelectedImageUri, preset); @@ -340,7 +347,7 @@ public class SaveCopyTask extends AsyncTask { // If we succeed in writing the bitmap as a jpeg, return a uri. if (putExifData(mDestinationFile, exif, bitmap)) { putPanoramaXMPData(mDestinationFile, xmp); - uri = insertContent(mContext, mSelectedImageUri, mDestinationFile, + uri = SaveCopyTask.insertContent(mContext, mSelectedImageUri, mDestinationFile, time); } @@ -361,7 +368,7 @@ public class SaveCopyTask extends AsyncTask { throw e; } System.gc(); - options.inSampleSize *= 2; + sampleSize *= 2; } } return uri; @@ -434,7 +441,31 @@ public class SaveCopyTask extends AsyncTask { } } - private static void querySource(Context context, Uri sourceUri, String[] projection, + public static Uri makeAndInsertUri(Context context, Uri sourceUri) { + long time = System.currentTimeMillis(); + String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); + File saveDirectory = getFinalSaveDirectory(context, sourceUri); + File file = new File(saveDirectory, filename + ".JPG"); + return insertContent(context, sourceUri, file, time); + } + + public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, + File destination) { + Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); + new SaveCopyTask(filterShowActivity, MasterImage.getImage().getUri(), selectedImageUri, + destination, + new SaveCopyTask.Callback() { + + @Override + public void onComplete(Uri result) { + filterShowActivity.completeSaveImage(result); + } + + }).execute(preset); + } + + + public static void querySource(Context context, Uri sourceUri, String[] projection, ContentResolverQueryCallback callback) { ContentResolver contentResolver = context.getContentResolver(); querySourceFromContentResolver(contentResolver, sourceUri, projection, callback); @@ -537,8 +568,7 @@ public class SaveCopyTask extends AsyncTask { /** * Insert the content (saved file) with proper source photo properties. */ - private static Uri insertContent(Context context, Uri sourceUri, File file, - long time) { + public static Uri insertContent(Context context, Uri sourceUri, File file, long time) { time /= 1000; final ContentValues values = new ContentValues(); @@ -546,7 +576,7 @@ public class SaveCopyTask extends AsyncTask { values.put(Images.Media.DISPLAY_NAME, file.getName()); values.put(Images.Media.MIME_TYPE, "image/jpeg"); values.put(Images.Media.DATE_TAKEN, time); - values.put(Images.Media.DATE_MODIFIED, System.currentTimeMillis()); + values.put(Images.Media.DATE_MODIFIED, time); values.put(Images.Media.DATE_ADDED, time); values.put(Images.Media.ORIENTATION, 0); values.put(Images.Media.DATA, file.getAbsolutePath()); @@ -556,8 +586,8 @@ public class SaveCopyTask extends AsyncTask { ImageColumns.DATE_TAKEN, ImageColumns.LATITUDE, ImageColumns.LONGITUDE, }; - querySource(context, sourceUri, projection, - new ContentResolverQueryCallback() { + SaveCopyTask.querySource(context, sourceUri, projection, + new SaveCopyTask.ContentResolverQueryCallback() { @Override public void onCursorResult(Cursor cursor) { -- cgit v1.2.3