import android.app.WallpaperManager;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
import android.graphics.Matrix;
+import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputDevice.MotionRange;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
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.SaveImage;
+import com.google.android.pano.util.TouchNavGestureDetector;
+import com.google.android.pano.util.TouchNavGestureDetector.OnGestureListener;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
/**
* Activity for cropping an image.
*/
-public class CropActivity extends Activity {
+public class CropActivity extends Activity implements OnGestureListener {
private static final String LOGTAG = "CropActivity";
public static final String CROP_ACTION = "com.android.camera.action.CROP";
+ private static final int MIN_SCROLL_LENGTH = 5;
private CropExtras mCropExtras = null;
private LoadBitmapTask mLoadBitmapTask = null;
+ private int mOutputX = 0;
+ private int mOutputY = 0;
private Bitmap mOriginalBitmap = null;
private RectF mOriginalBounds = null;
private int mOriginalRotation = 0;
private static final int SELECT_PICTURE = 1; // request code for picker
private static final int DEFAULT_COMPRESS_QUALITY = 90;
- public static final int MAX_BMAP_IN_INTENT = 990000;
+ /**
+ * The maximum bitmap size we allow to be returned through the intent.
+ * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
+ * have some overhead to hit so that we go way below the limit here to make
+ * sure the intent stays below 1MB.We should consider just returning a byte
+ * array instead of a Bitmap instance to avoid overhead.
+ */
+ public static final int MAX_BMAP_IN_INTENT = 750000;
// Flags
private static final int DO_SET_WALLPAPER = 1;
private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
+ private TouchNavGestureDetector mGestureDetector;
+ private float mTouchPadToViewRatioX;
+ private float mTouchPadToViewRatioY;
+ private float mResolutionX;
+ private float mResolutionY;
+
+ /**
+ * Used for click filtering. Any scroll less than MIN_SCROLL_LENGTH will not
+ * be registered. This will make clicking motions not trigger a scroll. Once
+ * you get past MIN_SCROLL_LENGTH, you can go below it again and register
+ * scrolls. This is to allow for starting a scroll and then moving back to
+ * the origin of the scroll.
+ */
+ private boolean mDidStartDrag;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mGestureDetector = new TouchNavGestureDetector(this, this);
Intent intent = getIntent();
setResult(RESULT_CANCELED, new Intent());
mCropExtras = getExtrasFromIntent(intent);
mCropView = (CropView) findViewById(R.id.cropView);
ActionBar actionBar = getActionBar();
- actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
- actionBar.setCustomView(R.layout.filtershow_actionbar);
-
- View mSaveButton = actionBar.getCustomView();
- mSaveButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- startFinishOutput();
- }
- });
+ if (actionBar != null) {
+ actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+ mSaveButton = actionBar.getCustomView();
+ mSaveButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startFinishOutput();
+ }
+ });
+ }
if (intent.getData() != null) {
mSourceUri = intent.getData();
super.onDestroy();
}
+ @Override
+ public void onConfigurationChanged (Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mCropView.configChanged();
+ }
+
/**
* Opens a selector in Gallery to chose an image for use when none was given
* in the CROP intent.
if (mCropExtras != null) {
int aspectX = mCropExtras.getAspectX();
int aspectY = mCropExtras.getAspectY();
- int outputX = mCropExtras.getOutputX();
- int outputY = mCropExtras.getOutputY();
- if (outputX > 0 && outputY > 0) {
- mCropView.applyAspect(outputX, outputY);
+ mOutputX = mCropExtras.getOutputX();
+ mOutputY = mCropExtras.getOutputY();
+ if (mOutputX > 0 && mOutputY > 0) {
+ mCropView.applyAspect(mOutputX, mOutputY);
+
+ }
+ float spotX = mCropExtras.getSpotlightX();
+ float spotY = mCropExtras.getSpotlightY();
+ if (spotX > 0 && spotY > 0) {
+ mCropView.setWallpaperSpotlight(spotX, spotY);
}
if (aspectX > 0 && aspectY > 0) {
mCropView.applyAspect(aspectX, aspectY);
@Override
protected Bitmap doInBackground(Uri... params) {
Uri uri = params[0];
- Bitmap bmap = CropLoader.getConstrainedBitmap(uri, mContext, mBitmapSize,
- mOriginalBounds);
- mOrientation = CropLoader.getMetadataRotation(uri, mContext);
+ Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
+ mOriginalBounds, false);
+ mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
return bmap;
}
if (mOriginalBitmap != null && mCropExtras != null) {
if (mCropExtras.getExtraOutput() != null) {
destinationUri = mCropExtras.getExtraOutput();
- flags |= DO_EXTRA_OUTPUT;
+ if (destinationUri != null) {
+ flags |= DO_EXTRA_OUTPUT;
+ }
}
if (mCropExtras.getSetAsWallpaper()) {
flags |= DO_SET_WALLPAPER;
}
}
if (flags == 0) {
- destinationUri = CropLoader.makeAndInsertUri(this, mSourceUri);
+ destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
if (destinationUri != null) {
flags |= DO_EXTRA_OUTPUT;
}
}
- if ((flags & FLAG_CHECK) != 0) {
+ if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) {
RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
RectF crop = getBitmapCrop(photo);
startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
final View loading = findViewById(R.id.loading);
loading.setVisibility(View.VISIBLE);
BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
- photoBounds, currentBitmapBounds, rotation);
+ photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY);
ioTask.execute(currentBitmap);
}
Intent mResultIntent = null;
int mRotation = 0;
+ // Helper to setup input stream
+ private void regenerateInputStream() {
+ if (mInUri == null) {
+ Log.w(LOGTAG, "cannot read original file, no input URI given");
+ } else {
+ Utils.closeSilently(mInStream);
+ try {
+ mInStream = getContentResolver().openInputStream(mInUri);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+ }
+ }
+ }
+
public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
- RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation) {
+ RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
+ int outputX, int outputY) {
mOutputFormat = outputFormat;
mOutStream = null;
mOutUri = destUri;
mRotation = (rotation < 0) ? -rotation : rotation;
mRotation %= 360;
mRotation = 90 * (int) (mRotation / 90); // now mRotation is a multiple of 90
+ mOutputX = outputX;
+ mOutputY = outputY;
if ((flags & DO_EXTRA_OUTPUT) != 0) {
if (mOutUri == null) {
}
if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
- if (mInUri == null) {
- Log.w(LOGTAG, "cannot read original file, no input URI given");
- } else {
- try {
- mInStream = getContentResolver().openInputStream(mInUri);
- } catch (FileNotFoundException e) {
- Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
- }
- }
+ regenerateInputStream();
}
}
// Do the large cropped bitmap and/or set the wallpaper
if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
- BitmapRegionDecoder decoder = null;
- try {
- decoder = BitmapRegionDecoder.newInstance(mInStream, true);
- } catch (IOException e) {
- Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
- }
- if (decoder == null) {
- failure = true;
- return false;
- }
-
// Find crop bounds (scaled to original image size)
RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
if (trueCrop == null) {
failure = true;
return false;
}
- // Do region decoding to get crop bitmap
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inMutable = true;
- Bitmap crop = decoder.decodeRegion(roundedTrueCrop, options);
- decoder.recycle();
+
+ // Attempt to open a region decoder
+ BitmapRegionDecoder decoder = null;
+ try {
+ decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+ }
+
+ Bitmap crop = null;
+ if (decoder != null) {
+ // Do region decoding to get crop bitmap
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ crop = decoder.decodeRegion(roundedTrueCrop, options);
+ decoder.recycle();
+ }
if (crop == null) {
- Log.w(LOGTAG, "cannot region decode file: " + mInUri.toString());
+ // BitmapRegionDecoder has failed, try to crop in-memory
+ regenerateInputStream();
+ Bitmap fullSize = null;
+ if (mInStream != null) {
+ fullSize = BitmapFactory.decodeStream(mInStream);
+ }
+ if (fullSize != null) {
+ crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+ roundedTrueCrop.top, roundedTrueCrop.width(),
+ roundedTrueCrop.height());
+ }
+ }
+
+ if (crop == null) {
+ Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
failure = true;
return false;
}
- if (mRotation > 0) {
+ if (mOutputX > 0 && mOutputY > 0) {
+ Matrix m = new Matrix();
+ RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
+ if (mRotation > 0) {
+ m.setRotate(mRotation);
+ m.mapRect(cropRect);
+ }
+ RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
+ m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+ m.preRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+ (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+ if (tmp != null) {
+ Canvas c = new Canvas(tmp);
+ c.drawBitmap(crop, m, new Paint());
+ crop = tmp;
+ }
+ } else if (mRotation > 0) {
Matrix m = new Matrix();
m.setRotate(mRotation);
Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
@Override
protected void onPostExecute(Boolean result) {
+ Utils.closeSilently(mOutStream);
+ Utils.closeSilently(mInStream);
doneBitmapIO(result.booleanValue(), mResultIntent);
}
RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);
return scaledCrop;
}
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ boolean handled = super.onGenericMotionEvent(event);
+ if (!handled) {
+ handled = mGestureDetector.onGenericMotionEvent(event);
+ if (!handled
+ && (event.getSource() & TouchNavGestureDetector.SOURCE_TOUCH_NAVIGATION)
+ == TouchNavGestureDetector.SOURCE_TOUCH_NAVIGATION) {
+ int action = event.getActionMasked();
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ sendScaledMotionEvent(event);
+ handled = true;
+ break;
+ }
+ }
+ }
+ return handled;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean handled = super.onKeyDown(keyCode, event);
+ if (!handled) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
+ startFinishOutput();
+ handled = true;
+ }
+ }
+ return handled;
+ }
+
+ /**
+ * Returns whether a scroll happened.
+ */
+ @Override
+ public boolean onScroll(MotionEvent downEvent, MotionEvent currentEvent, float deltaX,
+ float deltaY) {
+ // If <5mm from the start, consider it a click. Used for smoother
+ // clicks.
+ if (!mDidStartDrag) {
+ float distanceX = Math.abs((currentEvent.getX() - downEvent.getX()) / mResolutionX);
+ float distanceY = Math.abs((currentEvent.getY() - downEvent.getY()) / mResolutionY);
+ if (distanceX < MIN_SCROLL_LENGTH && distanceY < MIN_SCROLL_LENGTH) {
+ // Not moved enough to scroll.
+ return false;
+ }
+ }
+ // Moved enough to scroll.
+ mDidStartDrag = true;
+ sendScaledMotionEvent(currentEvent);
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent downEvent, MotionEvent currentEvent, float velX,
+ float velY) {
+ sendScaledMotionEvent(currentEvent);
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent downEvent) {
+ mDidStartDrag = false;
+ InputDevice device = downEvent.getDevice();
+ MotionRange motionRangeX = device.getMotionRange(MotionEvent.AXIS_X);
+ MotionRange motionRangeY = device.getMotionRange(MotionEvent.AXIS_Y);
+
+ if (motionRangeX == null || motionRangeY == null) {
+ return false;
+ }
+
+ mResolutionX = motionRangeX.getResolution();
+ mResolutionY = motionRangeY.getResolution();
+
+ if (mResolutionX == 0) {
+ mResolutionX = TouchNavGestureDetector.DEFAULT_TOUCH_RESOLUTION;
+ }
+ if (mResolutionY == 0) {
+ mResolutionY = TouchNavGestureDetector.DEFAULT_TOUCH_RESOLUTION;
+ }
+
+ // This is the conversion between a drag on the device vs. a drag on
+ // the screen.
+ mTouchPadToViewRatioX = mCropView.getWidth() / motionRangeX.getRange();
+ mTouchPadToViewRatioY = mCropView.getHeight() / motionRangeY.getRange();
+
+ sendScaledMotionEvent(downEvent);
+ return true;
+ }
+
+ private void sendScaledMotionEvent(MotionEvent event) {
+ final int scaledX = (int) (mTouchPadToViewRatioX * event.getX());
+ final int scaledY = (int) (mTouchPadToViewRatioY * event.getY());
+ final MotionEvent ev = MotionEvent.obtain(event.getEventTime(), event.getEventTime(),
+ event.getActionMasked(), scaledX, scaledY, 0);
+ mCropView.onTouchEvent(ev);
+ ev.recycle();
+ }
+
}