diff options
author | Michael Jurka <mikejurka@google.com> | 2013-10-30 14:40:39 +0100 |
---|---|---|
committer | Michael Jurka <mikejurka@google.com> | 2013-10-30 14:54:21 +0100 |
commit | 7b215cb92288aa0a21bc773511ddd537b8fbb459 (patch) | |
tree | 782eaddd66fc3c1b685630054126b1653833d9d3 /packages/WallpaperCropper | |
parent | 0e29faacecdb728e2878c799da8532e33539f046 (diff) | |
download | frameworks_base-7b215cb92288aa0a21bc773511ddd537b8fbb459.zip frameworks_base-7b215cb92288aa0a21bc773511ddd537b8fbb459.tar.gz frameworks_base-7b215cb92288aa0a21bc773511ddd537b8fbb459.tar.bz2 |
Make wallpaper cropper more robust
- don't crash if image passed to wallpaper picker
is invalid
- close input streams correctly
Bug: 11413915
Bug: 11380658
Bug: 11362731
Diffstat (limited to 'packages/WallpaperCropper')
3 files changed, 240 insertions, 87 deletions
diff --git a/packages/WallpaperCropper/res/values/strings.xml b/packages/WallpaperCropper/res/values/strings.xml index 2b8111d..091869a 100644 --- a/packages/WallpaperCropper/res/values/strings.xml +++ b/packages/WallpaperCropper/res/values/strings.xml @@ -17,4 +17,9 @@ <string name="crop_wallpaper">Crop wallpaper</string> <!-- Button label on Wallpaper picker screen; user selects this button to set a specific wallpaper --> <string name="wallpaper_instructions">Set wallpaper</string> + <!-- Error message when an image is selected as a wallpaper, + but the wallpaper cropper cannot load it. The user will + usually see this when using another app and trying to set + an image as the wallpaper --> + <string name="wallpaper_load_fail">Couldn\'t load image as wallpaper</string> </resources> diff --git a/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java b/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java index b5774f4..e14f89a 100644 --- a/packages/WallpaperCropper/src/com/android/photos/BitmapRegionTileSource.java +++ b/packages/WallpaperCropper/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; @@ -41,6 +42,85 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +interface SimpleBitmapRegionDecoder { + int getWidth(); + int getHeight(); + Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options); +} + +class SimpleBitmapRegionDecoderWrapper implements SimpleBitmapRegionDecoder { + BitmapRegionDecoder mDecoder; + private SimpleBitmapRegionDecoderWrapper(BitmapRegionDecoder decoder) { + mDecoder = decoder; + } + public static SimpleBitmapRegionDecoderWrapper newInstance(String pathName, boolean isShareable) { + try { + BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(pathName, isShareable); + if (d != null) { + return new SimpleBitmapRegionDecoderWrapper(d); + } + } catch (IOException e) { + Log.w("BitmapRegionTileSource", "getting decoder failed for path " + pathName, e); + return null; + } + return null; + } + public static SimpleBitmapRegionDecoderWrapper newInstance(InputStream is, boolean isShareable) { + try { + BitmapRegionDecoder d = BitmapRegionDecoder.newInstance(is, isShareable); + if (d != null) { + return new SimpleBitmapRegionDecoderWrapper(d); + } + } catch (IOException e) { + Log.w("BitmapRegionTileSource", "getting decoder failed", e); + return null; + } + return null; + } + public int getWidth() { + return mDecoder.getWidth(); + } + public int getHeight() { + return mDecoder.getHeight(); + } + public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { + return mDecoder.decodeRegion(wantRegion, options); + } +} + +class DumbBitmapRegionDecoder implements SimpleBitmapRegionDecoder { + //byte[] streamCopy; + Bitmap mBuffer; + private DumbBitmapRegionDecoder(Bitmap b) { + mBuffer = b; + } + public static DumbBitmapRegionDecoder newInstance(String pathName) { + Bitmap b = BitmapFactory.decodeFile(pathName); + if (b != null) { + return new DumbBitmapRegionDecoder(b); + } + return null; + } + public static DumbBitmapRegionDecoder newInstance(InputStream is) { + Bitmap b = BitmapFactory.decodeStream(is); + if (b != null) { + return new DumbBitmapRegionDecoder(b); + } + return null; + } + public int getWidth() { + return mBuffer.getWidth(); + } + public int getHeight() { + return mBuffer.getHeight(); + } + public Bitmap decodeRegion(Rect wantRegion, BitmapFactory.Options options) { + System.out.println("DECODING WITH SAMPLE LEVEL OF " + options.inSampleSize); + return Bitmap.createBitmap( + mBuffer, wantRegion.left, wantRegion.top, wantRegion.width(), wantRegion.height()); + } +} + /** * A {@link com.android.photos.views.TiledImageRenderer.TileSource} using * {@link BitmapRegionDecoder} to wrap a local file @@ -58,14 +138,16 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { public static final int MAX_PREVIEW_SIZE = GL_SIZE_LIMIT / 2; public static abstract class BitmapSource { - private BitmapRegionDecoder mDecoder; + private SimpleBitmapRegionDecoder mDecoder; 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,22 +156,33 @@ 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 BitmapRegionDecoder getBitmapRegionDecoder() { + public State getLoadingState() { + return mState; + } + + public SimpleBitmapRegionDecoder getBitmapRegionDecoder() { return mDecoder; } @@ -106,7 +199,7 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } public abstract boolean readExif(ExifInterface ei); - public abstract BitmapRegionDecoder loadBitmapRegionDecoder(); + public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder(); public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options); } @@ -117,13 +210,13 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { mPath = path; } @Override - public BitmapRegionDecoder loadBitmapRegionDecoder() { - try { - return BitmapRegionDecoder.newInstance(mPath, true); - } catch (IOException e) { - Log.w("BitmapRegionTileSource", "getting decoder failed", e); - return null; + public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { + SimpleBitmapRegionDecoder d; + d = SimpleBitmapRegionDecoderWrapper.newInstance(mPath, true); + if (d == null) { + d = DumbBitmapRegionDecoder.newInstance(mPath); } + return d; } @Override public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { @@ -154,9 +247,17 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { return new BufferedInputStream(is); } @Override - public BitmapRegionDecoder loadBitmapRegionDecoder() { + public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { try { - return BitmapRegionDecoder.newInstance(regenerateInputStream(), true); + InputStream is = regenerateInputStream(); + SimpleBitmapRegionDecoder regionDecoder = + SimpleBitmapRegionDecoderWrapper.newInstance(is, false); + Utils.closeSilently(is); + if (regionDecoder == null) { + is = regenerateInputStream(); + regionDecoder = DumbBitmapRegionDecoder.newInstance(is); + } + return regionDecoder; } catch (FileNotFoundException e) { Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e); return null; @@ -168,7 +269,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 +281,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; } } @@ -202,13 +308,16 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { return new BufferedInputStream(is); } @Override - public BitmapRegionDecoder loadBitmapRegionDecoder() { - try { - return BitmapRegionDecoder.newInstance(regenerateInputStream(), true); - } catch (IOException e) { - Log.e("BitmapRegionTileSource", "Error reading resource", e); - return null; + public SimpleBitmapRegionDecoder loadBitmapRegionDecoder() { + InputStream is = regenerateInputStream(); + SimpleBitmapRegionDecoder regionDecoder = + SimpleBitmapRegionDecoderWrapper.newInstance(is, false); + Utils.closeSilently(is); + if (regionDecoder == null) { + is = regenerateInputStream(); + regionDecoder = DumbBitmapRegionDecoder.newInstance(is); } + return regionDecoder; } @Override public Bitmap loadPreviewBitmap(BitmapFactory.Options options) { @@ -217,7 +326,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); @@ -226,7 +337,7 @@ public class BitmapRegionTileSource implements TiledImageRenderer.TileSource { } } - BitmapRegionDecoder mDecoder; + SimpleBitmapRegionDecoder mDecoder; int mWidth; int mHeight; int mTileSize; @@ -243,27 +354,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())); + } } } } diff --git a/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java b/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java index 220f567..d12140d 100644 --- a/packages/WallpaperCropper/src/com/android/wallpapercropper/WallpaperCropActivity.java +++ b/packages/WallpaperCropper/src/com/android/wallpapercropper/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<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() { @@ -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; |