From 619a180be5a67f5bcd7021d300bd35e8b5296e7f Mon Sep 17 00:00:00 2001 From: Michael Jurka Date: Tue, 29 Oct 2013 15:50:06 +0100 Subject: Make wallpaper picker/cropper more robust - don't crash if image passed to wallpaper picker is invalid - close input streams correctly Bug: 11413915 Bug: 11380658 Bug: 11362731 Change-Id: I973e6bdc532d24a64efd6d174e89fdac626d7ee3 --- res/values/strings.xml | 8 ++ .../android/launcher3/SavedWallpaperImages.java | 2 +- .../android/launcher3/WallpaperCropActivity.java | 97 ++++++++++++------ .../android/launcher3/WallpaperPickerActivity.java | 74 +++++++++----- src/com/android/photos/BitmapRegionTileSource.java | 109 +++++++++++++-------- 5 files changed, 195 insertions(+), 95 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index cafa42442..1997c8b9a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -30,6 +30,14 @@ Set wallpaper + + Coudn\'t load image + + Couldn\'t load image as wallpaper diff --git a/src/com/android/launcher3/SavedWallpaperImages.java b/src/com/android/launcher3/SavedWallpaperImages.java index 086d08580..58add7022 100644 --- a/src/com/android/launcher3/SavedWallpaperImages.java +++ b/src/com/android/launcher3/SavedWallpaperImages.java @@ -62,7 +62,7 @@ public class SavedWallpaperImages extends BaseAdapter implements ListAdapter { File file = new File(a.getFilesDir(), imageFilename); BitmapRegionTileSource.FilePathBitmapSource bitmapSource = new BitmapRegionTileSource.FilePathBitmapSource(file.getAbsolutePath(), 1024); - a.setCropViewTileSource(bitmapSource, false, true); + a.setCropViewTileSource(bitmapSource, false, true, null); } @Override public void onSave(WallpaperPickerActivity a) { diff --git a/src/com/android/launcher3/WallpaperCropActivity.java b/src/com/android/launcher3/WallpaperCropActivity.java index 29e8c972a..491316d74 100644 --- a/src/com/android/launcher3/WallpaperCropActivity.java +++ b/src/com/android/launcher3/WallpaperCropActivity.java @@ -41,10 +41,12 @@ import android.util.Log; import android.view.Display; import android.view.View; import android.view.WindowManager; +import android.widget.Toast; import com.android.gallery3d.common.Utils; import com.android.gallery3d.exif.ExifInterface; import com.android.photos.BitmapRegionTileSource; +import com.android.photos.BitmapRegionTileSource.BitmapSource; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; @@ -109,12 +111,24 @@ public class WallpaperCropActivity extends Activity { }); // Load image in background - setCropViewTileSource( - new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024), true, false); + final BitmapRegionTileSource.UriBitmapSource bitmapSource = + new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024); + Runnable onLoad = new Runnable() { + public void run() { + if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) { + Toast.makeText(WallpaperCropActivity.this, + getString(R.string.wallpaper_load_fail), + Toast.LENGTH_LONG).show(); + finish(); + } + } + }; + setCropViewTileSource(bitmapSource, true, false, onLoad); } - public void setCropViewTileSource(final BitmapRegionTileSource.BitmapSource bitmapSource, - final boolean touchEnabled, final boolean moveToLeft) { + public void setCropViewTileSource( + final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled, + final boolean moveToLeft, final Runnable postExecute) { final Context context = WallpaperCropActivity.this; final View progressView = findViewById(R.id.loading); final AsyncTask loadBitmapTask = new AsyncTask() { @@ -127,13 +141,18 @@ public class WallpaperCropActivity extends Activity { protected void onPostExecute(Void arg) { if (!isCancelled()) { progressView.setVisibility(View.INVISIBLE); - mCropView.setTileSource( - new BitmapRegionTileSource(context, bitmapSource), null); - mCropView.setTouchEnabled(touchEnabled); - if (moveToLeft) { - mCropView.moveToLeft(); + if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + mCropView.setTileSource( + new BitmapRegionTileSource(context, bitmapSource), null); + mCropView.setTouchEnabled(touchEnabled); + if (moveToLeft) { + mCropView.moveToLeft(); + } } } + if (postExecute != null) { + postExecute.run(); + } } }; // We don't want to show the spinner every time we load an image, because that would be @@ -235,10 +254,12 @@ public class WallpaperCropActivity extends Activity { InputStream is = context.getContentResolver().openInputStream(uri); BufferedInputStream bis = new BufferedInputStream(is); ei.readExif(bis); + bis.close(); } else { InputStream is = res.openRawResource(resId); BufferedInputStream bis = new BufferedInputStream(is); ei.readExif(bis); + bis.close(); } Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); if (ori != null) { @@ -408,7 +429,6 @@ public class WallpaperCropActivity extends Activity { String mInFilePath; byte[] mInImageBytes; int mInResId = 0; - InputStream mInStream; RectF mCropBounds = null; int mOutWidth, mOutHeight; int mRotation; @@ -481,37 +501,36 @@ public class WallpaperCropActivity extends Activity { } // Helper to setup input stream - private void regenerateInputStream() { + private InputStream regenerateInputStream() { if (mInUri == null && mInResId == 0 && mInFilePath == null && mInImageBytes == null) { Log.w(LOGTAG, "cannot read original file, no input URI, resource ID, or " + "image byte array given"); } else { - Utils.closeSilently(mInStream); try { if (mInUri != null) { - mInStream = new BufferedInputStream( + return new BufferedInputStream( mContext.getContentResolver().openInputStream(mInUri)); } else if (mInFilePath != null) { - mInStream = mContext.openFileInput(mInFilePath); + return mContext.openFileInput(mInFilePath); } else if (mInImageBytes != null) { - mInStream = new BufferedInputStream( - new ByteArrayInputStream(mInImageBytes)); + return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes)); } else { - mInStream = new BufferedInputStream( - mResources.openRawResource(mInResId)); + return new BufferedInputStream(mResources.openRawResource(mInResId)); } } catch (FileNotFoundException e) { Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); } } + return null; } public Point getImageBounds() { - regenerateInputStream(); - if (mInStream != null) { + InputStream is = regenerateInputStream(); + if (is != null) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(mInStream, null, options); + BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); if (options.outWidth != 0 && options.outHeight != 0) { return new Point(options.outWidth, options.outHeight); } @@ -529,22 +548,26 @@ public class WallpaperCropActivity extends Activity { public boolean cropBitmap() { boolean failure = false; - regenerateInputStream(); WallpaperManager wallpaperManager = null; if (mSetWallpaper) { wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext()); } - if (mSetWallpaper && mNoCrop && mInStream != null) { + + + if (mSetWallpaper && mNoCrop) { try { - wallpaperManager.setStream(mInStream); + InputStream is = regenerateInputStream(); + if (is != null) { + wallpaperManager.setStream(is); + Utils.closeSilently(is); + } } catch (IOException e) { Log.w(LOGTAG, "cannot write stream to wallpaper", e); failure = true; } return !failure; - } - if (mInStream != null) { + } else { // Find crop bounds (scaled to original image size) Rect roundedTrueCrop = new Rect(); Matrix rotateMatrix = new Matrix(); @@ -557,6 +580,11 @@ public class WallpaperCropActivity extends Activity { mCropBounds = new RectF(roundedTrueCrop); Point bounds = getImageBounds(); + if (bounds == null) { + Log.w(LOGTAG, "cannot get bounds for image"); + failure = true; + return false; + } float[] rotatedBounds = new float[] { bounds.x, bounds.y }; rotateMatrix.mapPoints(rotatedBounds); @@ -567,7 +595,6 @@ public class WallpaperCropActivity extends Activity { inverseRotateMatrix.mapRect(mCropBounds); mCropBounds.offset(bounds.x/2, bounds.y/2); - regenerateInputStream(); } mCropBounds.roundOut(roundedTrueCrop); @@ -585,7 +612,14 @@ public class WallpaperCropActivity extends Activity { // Attempt to open a region decoder BitmapRegionDecoder decoder = null; try { - decoder = BitmapRegionDecoder.newInstance(mInStream, true); + InputStream is = regenerateInputStream(); + if (is == null) { + Log.w(LOGTAG, "cannot get input stream for uri=" + mInUri.toString()); + failure = true; + return false; + } + decoder = BitmapRegionDecoder.newInstance(is, false); + Utils.closeSilently(is); } catch (IOException e) { Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); } @@ -603,14 +637,15 @@ public class WallpaperCropActivity extends Activity { if (crop == null) { // BitmapRegionDecoder has failed, try to crop in-memory - regenerateInputStream(); + InputStream is = regenerateInputStream(); Bitmap fullSize = null; - if (mInStream != null) { + if (is != null) { BitmapFactory.Options options = new BitmapFactory.Options(); if (scaleDownSampleSize > 1) { options.inSampleSize = scaleDownSampleSize; } - fullSize = BitmapFactory.decodeStream(mInStream, null, options); + fullSize = BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); } if (fullSize != null) { mCropBounds.left /= scaleDownSampleSize; diff --git a/src/com/android/launcher3/WallpaperPickerActivity.java b/src/com/android/launcher3/WallpaperPickerActivity.java index c58d66063..efc311070 100644 --- a/src/com/android/launcher3/WallpaperPickerActivity.java +++ b/src/com/android/launcher3/WallpaperPickerActivity.java @@ -67,8 +67,10 @@ import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; +import android.widget.Toast; import com.android.photos.BitmapRegionTileSource; +import com.android.photos.BitmapRegionTileSource.BitmapSource; import java.io.File; import java.io.FileOutputStream; @@ -84,7 +86,7 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES"; private static final String DEFAULT_WALLPAPER_THUMBNAIL_FILENAME = "default_thumb.jpg"; - private View mSelectedThumb; + private View mSelectedTile; private boolean mIgnoreNextTap; private OnClickListener mThumbnailOnClickListener; @@ -128,13 +130,39 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { public static class UriWallpaperInfo extends WallpaperTileInfo { private Uri mUri; + private boolean mFirstClick = true; + private BitmapRegionTileSource.UriBitmapSource mBitmapSource; public UriWallpaperInfo(Uri uri) { mUri = uri; } @Override - public void onClick(WallpaperPickerActivity a) { - a.setCropViewTileSource(new BitmapRegionTileSource.UriBitmapSource( - a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE), true, false); + public void onClick(final WallpaperPickerActivity a) { + final Runnable onLoad; + if (!mFirstClick) { + onLoad = null; + } else { + mFirstClick = false; + onLoad = new Runnable() { + public void run() { + if (mBitmapSource != null && + mBitmapSource.getLoadingState() == BitmapSource.State.LOADED) { + mView.setVisibility(View.VISIBLE); + a.selectTile(mView); + } else { + ViewGroup parent = (ViewGroup) mView.getParent(); + if (parent != null) { + parent.removeView(mView); + Toast.makeText(a, + a.getString(R.string.image_load_fail), + Toast.LENGTH_SHORT).show(); + } + } + } + }; + } + mBitmapSource = new BitmapRegionTileSource.UriBitmapSource( + a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE); + a.setCropViewTileSource(mBitmapSource, true, false, onLoad); } @Override public void onSave(final WallpaperPickerActivity a) { @@ -304,17 +332,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { return; } WallpaperTileInfo info = (WallpaperTileInfo) v.getTag(); - if (info.isSelectable()) { - if (mSelectedThumb != null) { - mSelectedThumb.setSelected(false); - mSelectedThumb = null; - } - mSelectedThumb = v; - v.setSelected(true); - // TODO: Remove this once the accessibility framework and - // services have better support for selection state. - v.announceForAccessibility( - getString(R.string.announce_selection, v.getContentDescription())); + if (info.isSelectable() && v.getVisibility() == View.VISIBLE) { + selectTile(v); } info.onClick(WallpaperPickerActivity.this); } @@ -440,8 +459,8 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { new View.OnClickListener() { @Override public void onClick(View v) { - if (mSelectedThumb != null) { - WallpaperTileInfo info = (WallpaperTileInfo) mSelectedThumb.getTag(); + if (mSelectedTile != null) { + WallpaperTileInfo info = (WallpaperTileInfo) mSelectedTile.getTag(); info.onSave(WallpaperPickerActivity.this); } } @@ -520,15 +539,23 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { CheckableFrameLayout c = (CheckableFrameLayout) mWallpapersView.getChildAt(i); c.setChecked(false); } - mSelectedThumb.setSelected(true); + mSelectedTile.setSelected(true); mActionMode = null; } }; } - @Override - public void setCropViewTileSource(final BitmapRegionTileSource.BitmapSource bitmapSource, - final boolean touchEnabled, boolean moveToLeft) { - super.setCropViewTileSource(bitmapSource, touchEnabled, moveToLeft); + + private void selectTile(View v) { + if (mSelectedTile != null) { + mSelectedTile.setSelected(false); + mSelectedTile = null; + } + mSelectedTile = v; + v.setSelected(true); + // TODO: Remove this once the accessibility framework and + // services have better support for selection state. + v.announceForAccessibility( + getString(R.string.announce_selection, v.getContentDescription())); } private void initializeScrollForRtl() { @@ -692,8 +719,9 @@ public class WallpaperPickerActivity extends WallpaperCropActivity { private void addTemporaryWallpaperTile(final Uri uri) { mTempWallpaperTiles.add(uri); // Add a tile for the image picked from Gallery - FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater(). + final FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater(). inflate(R.layout.wallpaper_picker_item, mWallpapersView, false); + pickedImageThumbnail.setVisibility(View.GONE); setWallpaperItemPaddingToZero(pickedImageThumbnail); mWallpapersView.addView(pickedImageThumbnail, 0); diff --git a/src/com/android/photos/BitmapRegionTileSource.java b/src/com/android/photos/BitmapRegionTileSource.java index b5774f40a..b85caaa1c 100644 --- a/src/com/android/photos/BitmapRegionTileSource.java +++ b/src/com/android/photos/BitmapRegionTileSource.java @@ -31,6 +31,7 @@ import android.os.Build.VERSION_CODES; import android.util.Log; import com.android.gallery3d.common.BitmapUtils; +import com.android.gallery3d.common.Utils; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.glrenderer.BasicTexture; import com.android.gallery3d.glrenderer.BitmapTexture; @@ -62,10 +63,12 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { private Bitmap mPreview; private int mPreviewSize; private int mRotation; + public enum State { NOT_LOADED, LOADED, ERROR_LOADING }; + private State mState = State.NOT_LOADED; public BitmapSource(int previewSize) { mPreviewSize = previewSize; } - public void loadInBackground() { + public boolean loadInBackground() { ExifInterface ei = new ExifInterface(); if (readExif(ei)) { Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION); @@ -74,21 +77,32 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } } mDecoder = loadBitmapRegionDecoder(); - int width = mDecoder.getWidth(); - int height = mDecoder.getHeight(); - if (mPreviewSize != 0) { - int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); - BitmapFactory.Options opts = new BitmapFactory.Options(); - opts.inPreferredConfig = Bitmap.Config.ARGB_8888; - opts.inPreferQualityOverSpeed = true; - - float scale = (float) previewSize / Math.max(width, height); - opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); - opts.inJustDecodeBounds = false; - mPreview = loadPreviewBitmap(opts); + if (mDecoder == null) { + mState = State.ERROR_LOADING; + return false; + } else { + int width = mDecoder.getWidth(); + int height = mDecoder.getHeight(); + if (mPreviewSize != 0) { + int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE); + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inPreferredConfig = Bitmap.Config.ARGB_8888; + opts.inPreferQualityOverSpeed = true; + + float scale = (float) previewSize / Math.max(width, height); + opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale); + opts.inJustDecodeBounds = false; + mPreview = loadPreviewBitmap(opts); + } + mState = State.LOADED; + return true; } } + public State getLoadingState() { + return mState; + } + public BitmapRegionDecoder getBitmapRegionDecoder() { return mDecoder; } @@ -156,7 +170,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public BitmapRegionDecoder loadBitmapRegionDecoder() { try { - return BitmapRegionDecoder.newInstance(regenerateInputStream(), true); + InputStream is = regenerateInputStream(); + BitmapRegionDecoder regionDecoder = BitmapRegionDecoder.newInstance(is, false); + Utils.closeSilently(is); + return regionDecoder; } catch (FileNotFoundException e) { Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return null; @@ -168,7 +185,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { try { - return BitmapFactory.decodeStream(regenerateInputStream(), null, options); + InputStream is = regenerateInputStream(); + Bitmap b = BitmapFactory.decodeStream(is, null, options); + Utils.closeSilently(is); + return b; } catch (FileNotFoundException e) { Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return null; @@ -177,13 +197,15 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public boolean readExif(ExifInterface ei) { try { - ei.readExif(regenerateInputStream()); + InputStream is = regenerateInputStream(); + ei.readExif(is); + Utils.closeSilently(is); return true; } catch (FileNotFoundException e) { Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return false; } catch (IOException e) { - Log.e("BitmapRegionTileSource", "Failure while reading URI " + mUri, e); + Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return false; } } @@ -204,7 +226,10 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public BitmapRegionDecoder loadBitmapRegionDecoder() { try { - return BitmapRegionDecoder.newInstance(regenerateInputStream(), true); + InputStream is = regenerateInputStream(); + BitmapRegionDecoder regionDecoder = BitmapRegionDecoder.newInstance(is, true); + Utils.closeSilently(is); + return regionDecoder; } catch (IOException e) { Log.e("BitmapRegionTileSource", "Error reading resource", e); return null; @@ -217,7 +242,9 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { @Override public boolean readExif(ExifInterface ei) { try { - ei.readExif(regenerateInputStream()); + InputStream is = regenerateInputStream(); + ei.readExif(is); + Utils.closeSilently(is); return true; } catch (IOException e) { Log.e("BitmapRegionTileSource", "Error reading resource", e); @@ -243,27 +270,29 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { mTileSize = TiledImageRenderer.suggestedTileSize(context); mRotation = source.getRotation(); mDecoder = source.getBitmapRegionDecoder(); - mWidth = mDecoder.getWidth(); - mHeight = mDecoder.getHeight(); - mOptions = new BitmapFactory.Options(); - mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; - mOptions.inPreferQualityOverSpeed = true; - mOptions.inTempStorage = new byte[16 * 1024]; - int previewSize = source.getPreviewSize(); - if (previewSize != 0) { - previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); - // Although this is the same size as the Bitmap that is likely already - // loaded, the lifecycle is different and interactions are on a different - // thread. Thus to simplify, this source will decode its own bitmap. - Bitmap preview = decodePreview(source, previewSize); - if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { - mPreview = new BitmapTexture(preview); - } else { - Log.w(TAG, String.format( - "Failed to create preview of apropriate size! " - + " in: %dx%d, out: %dx%d", - mWidth, mHeight, - preview.getWidth(), preview.getHeight())); + if (mDecoder != null) { + mWidth = mDecoder.getWidth(); + mHeight = mDecoder.getHeight(); + mOptions = new BitmapFactory.Options(); + mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888; + mOptions.inPreferQualityOverSpeed = true; + mOptions.inTempStorage = new byte[16 * 1024]; + int previewSize = source.getPreviewSize(); + if (previewSize != 0) { + previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE); + // Although this is the same size as the Bitmap that is likely already + // loaded, the lifecycle is different and interactions are on a different + // thread. Thus to simplify, this source will decode its own bitmap. + Bitmap preview = decodePreview(source, previewSize); + if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) { + mPreview = new BitmapTexture(preview); + } else { + Log.w(TAG, String.format( + "Failed to create preview of apropriate size! " + + " in: %dx%d, out: %dx%d", + mWidth, mHeight, + preview.getWidth(), preview.getHeight())); + } } } } -- cgit v1.2.3