Merge "Moving crop to a separate activity.  Refactoring." into gb-ub-photos-bryce
diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java
index 22bd650..7c0c15a 100644
--- a/src/com/android/camera/PhotoModule.java
+++ b/src/com/android/camera/PhotoModule.java
@@ -64,8 +64,8 @@
 import com.android.gallery3d.exif.ExifInterface;
 import com.android.gallery3d.exif.ExifTag;
 import com.android.gallery3d.exif.Rational;
-import com.android.gallery3d.filtershow.CropExtras;
 import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 import com.android.gallery3d.util.UsageStatistics;
 
 import java.io.File;
diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java
index 51591cc..001ce87 100644
--- a/src/com/android/gallery3d/app/AlbumPage.java
+++ b/src/com/android/gallery3d/app/AlbumPage.java
@@ -39,8 +39,8 @@
 import com.android.gallery3d.data.MediaObject;
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
-import com.android.gallery3d.filtershow.CropExtras;
 import com.android.gallery3d.filtershow.FilterShowActivity;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 import com.android.gallery3d.glrenderer.FadeTexture;
 import com.android.gallery3d.glrenderer.GLCanvas;
 import com.android.gallery3d.ui.ActionModeHandler;
diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java
index 1bbe8d2..91bc772 100644
--- a/src/com/android/gallery3d/app/Wallpaper.java
+++ b/src/com/android/gallery3d/app/Wallpaper.java
@@ -27,7 +27,7 @@
 
 import com.android.gallery3d.common.ApiHelper;
 import com.android.gallery3d.filtershow.FilterShowActivity;
-import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 
 /**
  * Wallpaper picker for the gallery application. This just redirects to the
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index b659432..874a7c9 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -55,6 +55,7 @@
 import com.android.gallery3d.filtershow.cache.CachingPipeline;
 import com.android.gallery3d.filtershow.cache.FilteringPipeline;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 import com.android.gallery3d.filtershow.editors.BasicEditor;
 import com.android.gallery3d.filtershow.editors.EditorCrop;
 import com.android.gallery3d.filtershow.editors.EditorDraw;
diff --git a/src/com/android/gallery3d/filtershow/imageshow/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
similarity index 94%
rename from src/com/android/gallery3d/filtershow/imageshow/BoundedRect.java
rename to src/com/android/gallery3d/filtershow/crop/BoundedRect.java
index e94d1ed..c2c768e 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/BoundedRect.java
+++ b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
@@ -13,11 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.gallery3d.filtershow.imageshow;
+package com.android.gallery3d.filtershow.crop;
 
 import android.graphics.Matrix;
+import android.graphics.Rect;
 import android.graphics.RectF;
 
+import com.android.gallery3d.filtershow.imageshow.GeometryMath;
+
 import java.util.Arrays;
 
 /**
@@ -30,11 +33,14 @@
     private RectF inner;
     private float[] innerRotated;
 
-    public BoundedRect() {
-        rot = 0;
-        outer = new RectF();
-        inner = new RectF();
-        innerRotated = new float[8];
+    public BoundedRect(float rotation, Rect outerRect, Rect innerRect) {
+        rot = rotation;
+        outer = new RectF(outerRect);
+        inner = new RectF(innerRect);
+        innerRotated = CropMath.getCornersFromRect(inner);
+        rotateInner();
+        if (!isConstrained())
+            reconstrain();
     }
 
     public BoundedRect(float rotation, RectF outerRect, RectF innerRect) {
@@ -73,10 +79,22 @@
             reconstrain();
     }
 
+    public void setToInner(RectF r) {
+        r.set(inner);
+    }
+
+    public void setToOuter(RectF r) {
+        r.set(outer);
+    }
+
     public RectF getInner() {
         return new RectF(inner);
     }
 
+    public RectF getOuter() {
+        return new RectF(outer);
+    }
+
     /**
      * Tries to move the inner rectangle by (dx, dy).  If this would cause it to leave
      * the bounding rectangle, snaps the inner rectangle to the edge of the bounding
diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
new file mode 100644
index 0000000..26659a6
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
@@ -0,0 +1,422 @@
+/*
+ * 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.app.ActionBar;
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Activity for cropping an image.
+ */
+public class CropActivity extends Activity {
+    private static final String LOGTAG = "CropActivity";
+    private CropExtras mCropExtras = null;
+    private LoadBitmapTask mLoadBitmapTask = null;
+    private SaveBitmapTask mSaveBitmapTask = null;
+    private SetWallpaperTask mSetWallpaperTask = null;
+    private Bitmap mOriginalBitmap = null;
+    private CropView mCropView = null;
+    private int mActiveBackgroundIO = 0;
+    private Intent mResultIntent = null;
+    private static final int SELECT_PICTURE = 1; // request code for picker
+    private static final int DEFAULT_DENSITY = 133;
+    private static final int DEFAULT_COMPRESS_QUALITY = 90;
+    public static final int MAX_BMAP_IN_INTENT = 990000;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        mResultIntent = new Intent();
+        setResult(RESULT_CANCELED, mResultIntent);
+        mCropExtras = getExtrasFromIntent(intent);
+        if (mCropExtras != null && mCropExtras.getShowWhenLocked()) {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        }
+
+        setContentView(R.layout.crop_activity);
+        mCropView = (CropView) findViewById(R.id.cropView);
+
+        if (intent.getData() != null) {
+            startLoadBitmap(intent.getData());
+        } else {
+            pickImage();
+        }
+        ActionBar actionBar = getActionBar();
+        actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+        actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+        View saveButton = actionBar.getCustomView();
+        saveButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                startFinishOutput();
+            }
+        });
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (mLoadBitmapTask != null) {
+            mLoadBitmapTask.cancel(false);
+        }
+        super.onDestroy();
+    }
+
+    /**
+     * Opens a selector in Gallery to chose an image for use when none was given
+     * in the CROP intent.
+     */
+    public void pickImage() {
+        Intent intent = new Intent();
+        intent.setType("image/*");
+        intent.setAction(Intent.ACTION_GET_CONTENT);
+        startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
+                SELECT_PICTURE);
+    }
+
+    /**
+     * Callback for pickImage().
+     */
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) {
+            Uri selectedImageUri = data.getData();
+            startLoadBitmap(selectedImageUri);
+        }
+    }
+
+    /**
+     * Gets the crop extras from the intent, or null if none exist.
+     */
+    public static CropExtras getExtrasFromIntent(Intent intent) {
+        Bundle extras = intent.getExtras();
+        if (extras != null) {
+            return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0),
+                    extras.getInt(CropExtras.KEY_OUTPUT_Y, 0),
+                    extras.getBoolean(CropExtras.KEY_SCALE, true) &&
+                            extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false),
+                    extras.getInt(CropExtras.KEY_ASPECT_X, 0),
+                    extras.getInt(CropExtras.KEY_ASPECT_Y, 0),
+                    extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false),
+                    extras.getBoolean(CropExtras.KEY_RETURN_DATA, false),
+                    (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT),
+                    extras.getString(CropExtras.KEY_OUTPUT_FORMAT),
+                    extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false),
+                    extras.getFloat(CropExtras.KEY_SPOTLIGHT_X),
+                    extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y));
+        }
+        return null;
+    }
+
+    /**
+     * Gets screen size metric.
+     */
+    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);
+        // TODO: WTF
+        return (DEFAULT_DENSITY * msize) / metrics.densityDpi + 512;
+    }
+
+    /**
+     * Method that loads a bitmap in an async task.
+     */
+    private void startLoadBitmap(Uri uri) {
+        mActiveBackgroundIO++;
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.VISIBLE);
+        mLoadBitmapTask = new LoadBitmapTask();
+        mLoadBitmapTask.execute(uri);
+    }
+
+    /**
+     * Method called on UI thread with loaded bitmap.
+     */
+    private void doneLoadBitmap(Bitmap bitmap) {
+        mActiveBackgroundIO--;
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.GONE);
+        mOriginalBitmap = bitmap;
+        // TODO: move these to dimens folder
+        if (bitmap != null) {
+            mCropView.setup(bitmap, (int) getPixelsFromDip(55), (int) getPixelsFromDip(25));
+        } else {
+            Log.w(LOGTAG, "could not load image for cropping");
+            cannotLoadImage();
+            setResult(RESULT_CANCELED, mResultIntent);
+            done();
+        }
+    }
+
+    /**
+     * Display toast for image loading failure.
+     */
+    private void cannotLoadImage() {
+        CharSequence text = getString(R.string.cannot_load_image);
+        Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
+        toast.show();
+    }
+
+    /**
+     * AsyncTask for loading a bitmap into memory.
+     *
+     * @see #startLoadBitmap(Uri)
+     * @see #doneLoadBitmap(Bitmap)
+     */
+    private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> {
+        int mBitmapSize;
+        Context mContext;
+        Rect mOriginalBounds;
+
+        public LoadBitmapTask() {
+            mBitmapSize = getScreenImageSize();
+            Log.v(LOGTAG, "bitmap size: " + mBitmapSize);
+            mContext = getApplicationContext();
+            mOriginalBounds = new Rect();
+        }
+
+        @Override
+        protected Bitmap doInBackground(Uri... params) {
+            Bitmap bmap = CropLoader.getConstrainedBitmap(params[0], mContext, mBitmapSize,
+                    mOriginalBounds);
+            return bmap;
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap result) {
+            doneLoadBitmap(result);
+            // super.onPostExecute(result);
+        }
+    }
+
+    private void startSaveBitmap(Bitmap bmap, Uri uri, String format) {
+        if (bmap == null || uri == null) {
+            throw new IllegalArgumentException("bad argument to startSaveBitmap");
+        }
+        mActiveBackgroundIO++;
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.VISIBLE);
+        mSaveBitmapTask = new SaveBitmapTask(uri, format);
+        mSaveBitmapTask.execute(bmap);
+    }
+
+    private void doneSaveBitmap(Uri uri) {
+        mActiveBackgroundIO--;
+        final View loading = findViewById(R.id.loading);
+        loading.setVisibility(View.GONE);
+        if (uri == null) {
+            Log.w(LOGTAG, "failed to save bitmap");
+            setResult(RESULT_CANCELED, mResultIntent);
+            done();
+            return;
+        }
+        done();
+    }
+
+    private class SaveBitmapTask extends AsyncTask<Bitmap, Void, Boolean> {
+
+        OutputStream mOutStream = null;
+        String mOutputFormat = null;
+        Uri mOutUri = null;
+
+        public SaveBitmapTask(Uri uri, String outputFormat) {
+            mOutputFormat = outputFormat;
+            mOutStream = null;
+            mOutUri = uri;
+            try {
+                mOutStream = getContentResolver().openOutputStream(uri);
+            } catch (FileNotFoundException e) {
+                Log.w(LOGTAG, "cannot write output: " + mOutUri.toString(), e);
+            }
+        }
+
+        @Override
+        protected Boolean doInBackground(Bitmap... params) {
+            if (mOutStream == null) {
+                return false;
+            }
+            CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
+            return params[0].compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream);
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+            if (result.booleanValue() == false) {
+                Log.w(LOGTAG, "could not compress to output: " + mOutUri.toString());
+                doneSaveBitmap(null);
+            }
+            doneSaveBitmap(mOutUri);
+        }
+    }
+
+    private void startSetWallpaper(Bitmap bmap) {
+        if (bmap == null) {
+            throw new IllegalArgumentException("bad argument to startSetWallpaper");
+        }
+        mActiveBackgroundIO++;
+        Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
+        mSetWallpaperTask = new SetWallpaperTask();
+        mSetWallpaperTask.execute(bmap);
+
+    }
+
+    private void doneSetWallpaper() {
+        mActiveBackgroundIO--;
+        done();
+    }
+
+    private class SetWallpaperTask extends AsyncTask<Bitmap, Void, Boolean> {
+        private final WallpaperManager mWPManager;
+
+        public SetWallpaperTask() {
+            mWPManager = WallpaperManager.getInstance(getApplicationContext());
+        }
+
+        @Override
+        protected Boolean doInBackground(Bitmap... params) {
+            try {
+                mWPManager.setBitmap(params[0]);
+            } catch (IOException e) {
+                Log.w(LOGTAG, "fail to set wall paper", e);
+            }
+            return true;
+        }
+
+        @Override
+        protected void onPostExecute(Boolean result) {
+            doneSetWallpaper();
+        }
+    }
+
+    private void startFinishOutput() {
+        if (mOriginalBitmap != null && mCropExtras != null) {
+            Bitmap cropped = null;
+            if (mCropExtras.getExtraOutput() != null) {
+                if (cropped == null) {
+                    cropped = getCroppedImage(mOriginalBitmap);
+                }
+                startSaveBitmap(cropped, mCropExtras.getExtraOutput(),
+                        mCropExtras.getOutputFormat());
+            }
+            if (mCropExtras.getSetAsWallpaper()) {
+                if (cropped == null) {
+                    cropped = getCroppedImage(mOriginalBitmap);
+                }
+                startSetWallpaper(cropped);
+            }
+            if (mCropExtras.getReturnData()) {
+                if (cropped == null) {
+                    cropped = getCroppedImage(mOriginalBitmap);
+                }
+                int bmapSize = cropped.getRowBytes() * cropped.getHeight();
+                if (bmapSize > MAX_BMAP_IN_INTENT) {
+                    Log.w(LOGTAG, "Bitmap too large to be returned via intent");
+                } else {
+                    mResultIntent.putExtra(CropExtras.KEY_DATA, cropped);
+                }
+            }
+            setResult(RESULT_OK, mResultIntent);
+        } else {
+            setResult(RESULT_CANCELED, mResultIntent);
+        }
+        done();
+    }
+
+    private void done() {
+        if (mActiveBackgroundIO == 0) {
+            finish();
+        }
+    }
+
+    private Bitmap getCroppedImage(Bitmap image) {
+        RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
+        RectF crop = getBitmapCrop(imageBounds);
+        if (crop == null) {
+            return image;
+        }
+        Rect intCrop = new Rect();
+        crop.roundOut(intCrop);
+        return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
+                intCrop.height());
+    }
+
+    private RectF getBitmapCrop(RectF imageBounds) {
+        RectF crop = new RectF();
+        if (!mCropView.getCropBounds(crop, imageBounds)) {
+            Log.w(LOGTAG, "could not get crop");
+            return null;
+        }
+        return crop;
+    }
+
+    /**
+     * Helper method for unit conversions.
+     */
+    public float getPixelsFromDip(float value) {
+        Resources r = getResources();
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
+                r.getDisplayMetrics());
+    }
+
+    private static CompressFormat convertExtensionToCompressFormat(String extension) {
+        return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
+    }
+
+    private static String getFileExtension(String requestFormat) {
+        String outputFormat = (requestFormat == null)
+                ? "jpg"
+                : requestFormat;
+        outputFormat = outputFormat.toLowerCase();
+        return (outputFormat.equals("png") || outputFormat.equals("gif"))
+                ? "png" // We don't support gif compression.
+                : "jpg";
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/CropExtras.java b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
similarity index 98%
rename from src/com/android/gallery3d/filtershow/CropExtras.java
rename to src/com/android/gallery3d/filtershow/crop/CropExtras.java
index 7ed8f1e..60fe9af 100644
--- a/src/com/android/gallery3d/filtershow/CropExtras.java
+++ b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.gallery3d.filtershow;
+package com.android.gallery3d.filtershow.crop;
 
 import android.net.Uri;
 
diff --git a/src/com/android/gallery3d/filtershow/crop/CropLoader.java b/src/com/android/gallery3d/filtershow/crop/CropLoader.java
new file mode 100644
index 0000000..4025493
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropLoader.java
@@ -0,0 +1,199 @@
+/*
+ * 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.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.provider.MediaStore;
+import android.util.Log;
+
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.exif.ExifInterface;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * This class contains reentrant static methods for loading a bitmap.
+ */
+public abstract class CropLoader {
+    public static final String LOGTAG = "CropLoader";
+    public static final String JPEG_MIME_TYPE = "image/jpeg";
+    public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT;
+    public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP;
+    public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT;
+    public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM;
+
+    /**
+     * 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 getMetadataOrientation(Uri uri, Context context) {
+        if (uri == null || context == null) {
+            throw new IllegalArgumentException("bad argument to getScaledBitmap");
+        }
+        if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
+            String mimeType = context.getContentResolver().getType(uri);
+            if (mimeType != JPEG_MIME_TYPE) {
+                return 0;
+            }
+            String path = uri.getPath();
+            int orientation = 0;
+            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;
+        }
+        Cursor cursor = null;
+        try {
+            cursor = context.getContentResolver().query(uri,
+                    new String[] {
+                        MediaStore.Images.ImageColumns.ORIENTATION
+                    },
+                    null, null, null);
+            if (cursor.moveToNext()) {
+                int ori = cursor.getInt(0);
+
+                switch (ori) {
+                    case 0:
+                        return ORI_NORMAL;
+                    case 90:
+                        return ORI_ROTATE_90;
+                    case 270:
+                        return ORI_ROTATE_270;
+                    case 180:
+                        return ORI_ROTATE_180;
+                    default:
+                        return 0;
+                }
+            }
+        } catch (SQLiteException e) {
+            return 0;
+        } catch (IllegalArgumentException e) {
+            return 0;
+        } finally {
+            Utils.closeSilently(cursor);
+        }
+        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);
+            if (imageSide > maxSideLength) {
+                int shifts = 1 + Integer.numberOfLeadingZeros(maxSideLength)
+                        - Integer.numberOfLeadingZeros(imageSide);
+                options.inSampleSize = 1 << shifts;
+            }
+
+            // Make sure sample size is reasonable
+            if (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;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/imageshow/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java
similarity index 87%
rename from src/com/android/gallery3d/filtershow/imageshow/CropMath.java
rename to src/com/android/gallery3d/filtershow/crop/CropMath.java
index 9037ca0..5914f1c 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/CropMath.java
+++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java
@@ -14,11 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.gallery3d.filtershow.imageshow;
+package com.android.gallery3d.filtershow.crop;
 
 import android.graphics.Matrix;
 import android.graphics.RectF;
 
+import com.android.gallery3d.filtershow.imageshow.GeometryMath;
+
 import java.util.Arrays;
 
 public class CropMath {
@@ -176,6 +178,33 @@
         r.set(centX - hw, centY - hh, centX + hw, centY + hh);
     }
 
+    /**
+     * Resizes rectangle to have a certain aspect ratio (center remains
+     * stationary) while constraining it to remain within the original rect.
+     *
+     * @param r rectangle to resize
+     * @param w new width aspect
+     * @param h new height aspect
+     */
+    public static void fixAspectRatioContained(RectF r, float w, float h) {
+        float origW = r.width();
+        float origH = r.height();
+        float origA = origW / origH;
+        float a = w / h;
+        float finalW = origW;
+        float finalH = origH;
+        if (origA < a) {
+            finalH = origH / a;
+        } else {
+            finalW = origW * a;
+        }
+        float centX = r.centerX();
+        float centY = r.centerY();
+        float hw = finalW / 2;
+        float hh = finalH / 2;
+        r.set(centX - hw, centY - hh, centX + hw, centY + hh);
+    }
+
     private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) {
         float dy = rotatedRect[1] - rotatedRect[3];
         float dx = rotatedRect[0] - rotatedRect[2];
diff --git a/src/com/android/gallery3d/filtershow/crop/CropObject.java b/src/com/android/gallery3d/filtershow/crop/CropObject.java
new file mode 100644
index 0000000..00baba9
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java
@@ -0,0 +1,327 @@
+/*
+ * 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.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMath;
+
+public class CropObject {
+
+    private BoundedRect mBoundedRect;
+    private float mAspectWidth = 1;
+    private float mAspectHeight = 1;
+    private boolean mFixAspectRatio = false;
+    private float mRotation = 0;
+    private float mTouchTolerance = 45;
+    private float mMinSideSize = 20;
+
+    public static final int MOVE_NONE = 0;
+    // Sides
+    public static final int MOVE_LEFT = 1;
+    public static final int MOVE_TOP = 2;
+    public static final int MOVE_RIGHT = 4;
+    public static final int MOVE_BOTTOM = 8;
+    public static final int MOVE_BLOCK = 16;
+
+    // Corners
+    public static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT;
+    public static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT;
+    public static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT;
+    public static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT;
+
+    private int mMovingEdges = MOVE_NONE;
+
+    public CropObject(Rect outerBound, Rect innerBound, int outerAngle) {
+        mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound);
+    }
+
+    public CropObject(RectF outerBound, RectF innerBound, int outerAngle) {
+        mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound);
+    }
+
+    public void setToInnerBounds(RectF r) {
+        mBoundedRect.setToInner(r);
+    }
+
+    public void setToOuterBounds(RectF r) {
+        mBoundedRect.setToOuter(r);
+    }
+
+    public RectF getInnerBounds() {
+        return mBoundedRect.getInner();
+    }
+
+    public RectF getOuterBounds() {
+        return mBoundedRect.getOuter();
+    }
+
+    public int getSelectState() {
+        return mMovingEdges;
+    }
+
+    public boolean isFixedAspect() {
+        return mFixAspectRatio;
+    }
+
+    public void rotateOuter(int angle) {
+        mRotation = angle % 360;
+        mBoundedRect.setRotation(mRotation);
+        clearSelectState();
+    }
+
+    public boolean setInnerAspectRatio(int width, int height) {
+        if (width <= 0 || height <= 0) {
+            throw new IllegalArgumentException("Width and Height must be greater than zero");
+        }
+        RectF inner = mBoundedRect.getInner();
+        CropMath.fixAspectRatioContained(inner, width, height);
+        if (inner.width() < mMinSideSize || inner.height() < mMinSideSize) {
+            return false;
+        }
+        mAspectWidth = width;
+        mAspectHeight = height;
+        mFixAspectRatio = true;
+        mBoundedRect.setInner(inner);
+        clearSelectState();
+        return true;
+    }
+
+    public void setTouchTolerance(float tolerance) {
+        if (tolerance <= 0) {
+            throw new IllegalArgumentException("Tolerance must be greater than zero");
+        }
+        mTouchTolerance = tolerance;
+    }
+
+    public void setMinInnerSideSize(float minSide) {
+        if (minSide <= 0) {
+            throw new IllegalArgumentException("Min dide must be greater than zero");
+        }
+        mMinSideSize = minSide;
+    }
+
+    public void unsetAspectRatio() {
+        mFixAspectRatio = false;
+        clearSelectState();
+    }
+
+    public boolean hasSelectedEdge() {
+        return mMovingEdges != MOVE_NONE;
+    }
+
+    public static boolean checkCorner(int selected) {
+        return selected == TOP_LEFT || selected == TOP_RIGHT || selected == BOTTOM_RIGHT
+                || selected == BOTTOM_LEFT;
+    }
+
+    public static boolean checkEdge(int selected) {
+        return selected == MOVE_LEFT || selected == MOVE_TOP || selected == MOVE_RIGHT
+                || selected == MOVE_BOTTOM;
+    }
+
+    public static boolean checkBlock(int selected) {
+        return selected == MOVE_BLOCK;
+    }
+
+    public static boolean checkValid(int selected) {
+        return selected == MOVE_NONE || checkBlock(selected) || checkEdge(selected)
+                || checkCorner(selected);
+    }
+
+    public void clearSelectState() {
+        mMovingEdges = MOVE_NONE;
+    }
+
+    public int wouldSelectEdge(float x, float y) {
+        int edgeSelected = calculateSelectedEdge(x, y);
+        if (edgeSelected != MOVE_NONE && edgeSelected != MOVE_BLOCK) {
+            return edgeSelected;
+        }
+        return MOVE_NONE;
+    }
+
+    public boolean selectEdge(int edge) {
+        if (!checkValid(edge)) {
+            // temporary
+            throw new IllegalArgumentException("bad edge selected");
+            // return false;
+        }
+        if ((mFixAspectRatio && !checkCorner(edge)) && !checkBlock(edge)) {
+            // temporary
+            throw new IllegalArgumentException("bad corner selected");
+            // return false;
+        }
+        mMovingEdges = edge;
+        return true;
+    }
+
+    public boolean selectEdge(float x, float y) {
+        int edgeSelected = calculateSelectedEdge(x, y);
+        if (mFixAspectRatio) {
+            edgeSelected = fixEdgeToCorner(edgeSelected);
+        }
+        if (edgeSelected == MOVE_NONE) {
+            return false;
+        }
+        return selectEdge(edgeSelected);
+    }
+
+    public boolean moveCurrentSelection(float dX, float dY) {
+        if (mMovingEdges == MOVE_NONE) {
+            return false;
+        }
+        RectF crop = mBoundedRect.getInner();
+
+        float minWidthHeight = mMinSideSize;
+
+        int movingEdges = mMovingEdges;
+        if (movingEdges == MOVE_BLOCK) {
+            mBoundedRect.moveInner(dX, dY);
+            return true;
+        } else {
+            float dx = 0;
+            float dy = 0;
+
+            if ((movingEdges & MOVE_LEFT) != 0) {
+                dx = Math.min(crop.left + dX, crop.right - minWidthHeight) - crop.left;
+            }
+            if ((movingEdges & MOVE_TOP) != 0) {
+                dy = Math.min(crop.top + dY, crop.bottom - minWidthHeight) - crop.top;
+            }
+            if ((movingEdges & MOVE_RIGHT) != 0) {
+                dx = Math.max(crop.right + dX, crop.left + minWidthHeight)
+                        - crop.right;
+            }
+            if ((movingEdges & MOVE_BOTTOM) != 0) {
+                dy = Math.max(crop.bottom + dY, crop.top + minWidthHeight)
+                        - crop.bottom;
+            }
+
+            if (mFixAspectRatio) {
+                float[] l1 = {
+                        crop.left, crop.bottom
+                };
+                float[] l2 = {
+                        crop.right, crop.top
+                };
+                if (movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT) {
+                    l1[1] = crop.top;
+                    l2[1] = crop.bottom;
+                }
+                float[] b = {
+                        l1[0] - l2[0], l1[1] - l2[1]
+                };
+                float[] disp = {
+                        dx, dy
+                };
+                float[] bUnit = GeometryMath.normalize(b);
+                float sp = GeometryMath.scalarProjection(disp, bUnit);
+                dx = sp * bUnit[0];
+                dy = sp * bUnit[1];
+                RectF newCrop = fixedCornerResize(crop, movingEdges, dx, dy);
+
+                mBoundedRect.fixedAspectResizeInner(newCrop);
+            } else {
+                if ((movingEdges & MOVE_LEFT) != 0) {
+                    crop.left += dx;
+                }
+                if ((movingEdges & MOVE_TOP) != 0) {
+                    crop.top += dy;
+                }
+                if ((movingEdges & MOVE_RIGHT) != 0) {
+                    crop.right += dx;
+                }
+                if ((movingEdges & MOVE_BOTTOM) != 0) {
+                    crop.bottom += dy;
+                }
+                mBoundedRect.resizeInner(crop);
+            }
+        }
+        return true;
+    }
+
+    // Helper methods
+
+    private int calculateSelectedEdge(float x, float y) {
+        RectF cropped = mBoundedRect.getInner();
+
+        float left = Math.abs(x - cropped.left);
+        float right = Math.abs(x - cropped.right);
+        float top = Math.abs(y - cropped.top);
+        float bottom = Math.abs(y - cropped.bottom);
+
+        int edgeSelected = MOVE_NONE;
+        // Check left or right.
+        if ((left <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top)
+                && ((y - mTouchTolerance) <= cropped.bottom) && (left < right)) {
+            edgeSelected |= MOVE_LEFT;
+        }
+        else if ((right <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top)
+                && ((y - mTouchTolerance) <= cropped.bottom)) {
+            edgeSelected |= MOVE_RIGHT;
+        }
+
+        // Check top or bottom.
+        if ((top <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left)
+                && ((x - mTouchTolerance) <= cropped.right) && (top < bottom)) {
+            edgeSelected |= MOVE_TOP;
+        }
+        else if ((bottom <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left)
+                && ((x - mTouchTolerance) <= cropped.right)) {
+            edgeSelected |= MOVE_BOTTOM;
+        }
+        return edgeSelected;
+    }
+
+    private static RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy) {
+        RectF newCrop = null;
+        // Fix opposite corner in place and move sides
+        if (moving_corner == BOTTOM_RIGHT) {
+            newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height()
+                    + dy);
+        } else if (moving_corner == BOTTOM_LEFT) {
+            newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height()
+                    + dy);
+        } else if (moving_corner == TOP_LEFT) {
+            newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy,
+                    r.right, r.bottom);
+        } else if (moving_corner == TOP_RIGHT) {
+            newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left
+                    + r.width() + dx, r.bottom);
+        }
+        return newCrop;
+    }
+
+    private static int fixEdgeToCorner(int moving_edges) {
+        if (moving_edges == MOVE_LEFT) {
+            moving_edges |= MOVE_TOP;
+        }
+        if (moving_edges == MOVE_TOP) {
+            moving_edges |= MOVE_LEFT;
+        }
+        if (moving_edges == MOVE_RIGHT) {
+            moving_edges |= MOVE_BOTTOM;
+        }
+        if (moving_edges == MOVE_BOTTOM) {
+            moving_edges |= MOVE_RIGHT;
+        }
+        return moving_edges;
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropView.java b/src/com/android/gallery3d/filtershow/crop/CropView.java
new file mode 100644
index 0000000..561f7ae
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropView.java
@@ -0,0 +1,278 @@
+/*
+ * 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.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.gallery3d.R;
+
+public class CropView extends View {
+    private static final String LOGTAG = "CropView";
+
+    Bitmap mImage = null;
+    CropObject mCropObj = null;
+    private final Drawable mCropIndicator;
+    private final int mIndicatorSize;
+
+    private float mPrevX = 0;
+    private float mPrevY = 0;
+
+    private int mMinSideSize = 45;
+    private int mTouchTolerance = 20;
+    private boolean mMovingBlock = false;
+
+    private Matrix mDisplayMatrix = null;
+    private Matrix mDisplayMatrixInverse = null;
+
+    private enum Mode {
+        NONE, MOVE
+    }
+
+    private Mode mState = Mode.NONE;
+
+    public CropView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        Resources resources = context.getResources();
+        mCropIndicator = resources.getDrawable(R.drawable.camera_crop);
+        mIndicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
+    }
+
+    // For unchanging parameters
+    public void setup(Bitmap image, int minSideSize, int touchTolerance) {
+        mImage = image;
+        mMinSideSize = minSideSize;
+        mTouchTolerance = touchTolerance;
+        reset();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        if (mImage == null) {
+            return;
+        }
+        int displayWidth = getWidth();
+        int displayHeight = getHeight();
+        Rect imageBoundsOriginal = new Rect(0, 0, mImage.getWidth(), mImage.getHeight());
+        Rect displayBoundsOriginal = new Rect(0, 0, displayWidth, displayHeight);
+        if (mCropObj == null) {
+            reset();
+            mCropObj = new CropObject(imageBoundsOriginal, imageBoundsOriginal, 0);
+        }
+
+        RectF imageBounds = mCropObj.getInnerBounds();
+        RectF displayBounds = mCropObj.getOuterBounds();
+
+        // If display matrix doesn't exist, create it and its dependencies
+        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+            mDisplayMatrix = getBitmapToDisplayMatrix(displayBounds, new RectF(
+                    displayBoundsOriginal));
+            mDisplayMatrixInverse = new Matrix();
+            mDisplayMatrixInverse.reset();
+            if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) {
+                Log.w(LOGTAG, "could not invert display matrix");
+            }
+            // Scale min side and tolerance by display matrix scale factor
+            mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
+            mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
+        }
+        canvas.drawBitmap(mImage, mDisplayMatrix, new Paint());
+
+        if (mDisplayMatrix.mapRect(imageBounds)) {
+            drawCropRect(canvas, imageBounds);
+            drawRuleOfThird(canvas, imageBounds);
+            drawIndicators(canvas, mCropIndicator, mIndicatorSize, imageBounds,
+                    mCropObj.isFixedAspect(), mCropObj.getSelectState());
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+            return true;
+        }
+        float[] touchPoint = {
+                x, y
+        };
+        mDisplayMatrixInverse.mapPoints(touchPoint);
+        x = touchPoint[0];
+        y = touchPoint[1];
+        switch (event.getActionMasked()) {
+            case (MotionEvent.ACTION_DOWN):
+                if (mState == Mode.NONE) {
+                    if (!mCropObj.selectEdge(x, y)) {
+                        mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK);
+                    }
+                    mPrevX = x;
+                    mPrevY = y;
+                    mState = Mode.MOVE;
+                } else {
+                    reset();
+                }
+                break;
+            case (MotionEvent.ACTION_UP):
+                if (mState == Mode.MOVE) {
+                    mCropObj.selectEdge(CropObject.MOVE_NONE);
+                    mMovingBlock = false;
+                    mPrevX = x;
+                    mPrevY = y;
+                    mState = Mode.NONE;
+                } else {
+                    reset();
+                }
+                break;
+            case (MotionEvent.ACTION_MOVE):
+                if (mState == Mode.MOVE) {
+                    float dx = x - mPrevX;
+                    float dy = y - mPrevY;
+                    mCropObj.moveCurrentSelection(dx, dy);
+                    mPrevX = x;
+                    mPrevY = y;
+                } else {
+                    reset();
+                }
+                break;
+            default:
+                reset();
+                break;
+        }
+        invalidate();
+        return true;
+    }
+
+    public void reset() {
+        Log.w(LOGTAG, "reset called");
+        mState = Mode.NONE;
+        mCropObj = null;
+        mDisplayMatrix = null;
+        mDisplayMatrixInverse = null;
+        mMovingBlock = false;
+        invalidate();
+    }
+
+    public boolean getCropBounds(RectF out_crop, RectF in_newContaining) {
+        Matrix m = new Matrix();
+        RectF inner = mCropObj.getInnerBounds();
+        RectF outer = mCropObj.getOuterBounds();
+        if (!m.setRectToRect(outer, in_newContaining, Matrix.ScaleToFit.FILL)) {
+            Log.w(LOGTAG, "failed to make transform matrix");
+            return false;
+        }
+        if (!m.mapRect(inner)) {
+            Log.w(LOGTAG, "failed to transform crop bounds");
+            return false;
+        }
+        out_crop.set(inner);
+        return true;
+    }
+
+    // Helper methods
+
+    private static void drawRuleOfThird(Canvas canvas, RectF bounds) {
+        Paint p = new Paint();
+        p.setStyle(Paint.Style.STROKE);
+        p.setColor(Color.argb(128, 255, 255, 255));
+        p.setStrokeWidth(2);
+        float stepX = bounds.width() / 3.0f;
+        float stepY = bounds.height() / 3.0f;
+        float x = bounds.left + stepX;
+        float y = bounds.top + stepY;
+        for (int i = 0; i < 2; i++) {
+            canvas.drawLine(x, bounds.top, x, bounds.bottom, p);
+            x += stepX;
+        }
+        for (int j = 0; j < 2; j++) {
+            canvas.drawLine(bounds.left, y, bounds.right, y, p);
+            y += stepY;
+        }
+    }
+
+    private static void drawCropRect(Canvas canvas, RectF bounds) {
+        Paint p = new Paint();
+        p.setStyle(Paint.Style.STROKE);
+        p.setColor(Color.WHITE);
+        p.setStrokeWidth(3);
+        canvas.drawRect(bounds, p);
+    }
+
+    private static void drawIndicator(Canvas canvas, Drawable indicator, int indicatorSize,
+            float centerX, float centerY) {
+        int left = (int) centerX - indicatorSize / 2;
+        int top = (int) centerY - indicatorSize / 2;
+        indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize);
+        indicator.draw(canvas);
+    }
+
+    private static void drawIndicators(Canvas canvas, Drawable cropIndicator, int indicatorSize,
+            RectF bounds, boolean fixedAspect, int selection) {
+        boolean notMoving = (selection == CropObject.MOVE_NONE);
+        if (fixedAspect) {
+            if ((selection == CropObject.TOP_LEFT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.top);
+            }
+            if ((selection == CropObject.TOP_RIGHT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.top);
+            }
+            if ((selection == CropObject.BOTTOM_LEFT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.bottom);
+            }
+            if ((selection == CropObject.BOTTOM_RIGHT) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.bottom);
+            }
+        } else {
+            if (((selection & CropObject.MOVE_TOP) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.top);
+            }
+            if (((selection & CropObject.MOVE_BOTTOM) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.bottom);
+            }
+            if (((selection & CropObject.MOVE_LEFT) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.centerY());
+            }
+            if (((selection & CropObject.MOVE_RIGHT) != 0) || notMoving) {
+                drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.centerY());
+            }
+        }
+    }
+
+    private static Matrix getBitmapToDisplayMatrix(RectF imageBounds, RectF displayBounds) {
+        Matrix m = new Matrix();
+        setBitmapToDisplayMatrix(m, imageBounds, displayBounds);
+        return m;
+    }
+
+    private static boolean setBitmapToDisplayMatrix(Matrix m, RectF imageBounds,
+            RectF displayBounds) {
+        m.reset();
+        return m.setRectToRect(imageBounds, displayBounds, Matrix.ScaleToFit.CENTER);
+    }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
index df922f1..e2173ad 100644
--- a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
+++ b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java
@@ -21,7 +21,7 @@
 import android.widget.FrameLayout;
 
 import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 import com.android.gallery3d.filtershow.imageshow.ImageCrop;
 import com.android.gallery3d.filtershow.imageshow.MasterImage;
 
diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
index 5a33cc8..4f46eed 100644
--- a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
+++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java
@@ -24,7 +24,7 @@
 import android.graphics.RectF;
 import android.util.Log;
 
-import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 import com.android.gallery3d.filtershow.imageshow.GeometryMath;
 import com.android.gallery3d.filtershow.imageshow.GeometryMetadata;
 
diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
index ad2152a..898fdf0 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java
@@ -21,8 +21,8 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 
-import com.android.gallery3d.filtershow.CropExtras;
 import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 import com.android.gallery3d.filtershow.editors.EditorCrop;
 import com.android.gallery3d.filtershow.editors.EditorFlip;
 import com.android.gallery3d.filtershow.editors.EditorRotate;
diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
index 2ea6f6a..6d62bbd 100644
--- a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
+++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java
@@ -33,7 +33,9 @@
 import android.widget.PopupMenu;
 
 import com.android.gallery3d.R;
-import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.crop.BoundedRect;
+import com.android.gallery3d.filtershow.crop.CropExtras;
+import com.android.gallery3d.filtershow.crop.CropMath;
 import com.android.gallery3d.filtershow.editors.EditorCrop;
 import com.android.gallery3d.filtershow.ui.FramedTextButton;
 
diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java
index 4818d26..eb81b6e 100644
--- a/src/com/android/gallery3d/gadget/WidgetConfigure.java
+++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java
@@ -36,7 +36,7 @@
 import com.android.gallery3d.data.MediaSet;
 import com.android.gallery3d.data.Path;
 import com.android.gallery3d.filtershow.FilterShowActivity;
-import com.android.gallery3d.filtershow.CropExtras;
+import com.android.gallery3d.filtershow.crop.CropExtras;
 
 public class WidgetConfigure extends Activity {
     @SuppressWarnings("unused")