Merge "Make wallpaper cropper more robust " into klp-dev

This commit is contained in:
Michael Jurka
2013-10-30 18:01:56 +00:00
committed by Android (Google) Code Review
3 changed files with 239 additions and 86 deletions

View File

@ -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>

View File

@ -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;
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);
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()));
}
}
}
}

View File

@ -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;