Moving some files and methods around

> Removing android.util package
> Moving static methods and classes out of WallpaperCropActivity
> Removing some unused utility methods

Change-Id: I252a0655ddce195189b6b3f0bf92970e5808c9d7
diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
new file mode 100644
index 0000000..45118bf
--- /dev/null
+++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
@@ -0,0 +1,405 @@
+/**
+ * Copyright (C) 2015 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.common;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.res.Resources;
+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.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
+
+    public interface OnBitmapCroppedHandler {
+        public void onBitmapCropped(byte[] imageBytes);
+    }
+
+    private static final int DEFAULT_COMPRESS_QUALITY = 90;
+    private static final String LOGTAG = "BitmapCropTask";
+
+    Uri mInUri = null;
+    Context mContext;
+    String mInFilePath;
+    byte[] mInImageBytes;
+    int mInResId = 0;
+    RectF mCropBounds = null;
+    int mOutWidth, mOutHeight;
+    int mRotation;
+    boolean mSetWallpaper;
+    boolean mSaveCroppedBitmap;
+    Bitmap mCroppedBitmap;
+    Runnable mOnEndRunnable;
+    Resources mResources;
+    BitmapCropTask.OnBitmapCroppedHandler mOnBitmapCroppedHandler;
+    boolean mNoCrop;
+
+    public BitmapCropTask(Context c, String filePath,
+            RectF cropBounds, int rotation, int outWidth, int outHeight,
+            boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+        mContext = c;
+        mInFilePath = filePath;
+        init(cropBounds, rotation,
+                outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+    }
+
+    public BitmapCropTask(byte[] imageBytes,
+            RectF cropBounds, int rotation, int outWidth, int outHeight,
+            boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+        mInImageBytes = imageBytes;
+        init(cropBounds, rotation,
+                outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+    }
+
+    public BitmapCropTask(Context c, Uri inUri,
+            RectF cropBounds, int rotation, int outWidth, int outHeight,
+            boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+        mContext = c;
+        mInUri = inUri;
+        init(cropBounds, rotation,
+                outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+    }
+
+    public BitmapCropTask(Context c, Resources res, int inResId,
+            RectF cropBounds, int rotation, int outWidth, int outHeight,
+            boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+        mContext = c;
+        mInResId = inResId;
+        mResources = res;
+        init(cropBounds, rotation,
+                outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
+    }
+
+    private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
+            boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
+        mCropBounds = cropBounds;
+        mRotation = rotation;
+        mOutWidth = outWidth;
+        mOutHeight = outHeight;
+        mSetWallpaper = setWallpaper;
+        mSaveCroppedBitmap = saveCroppedBitmap;
+        mOnEndRunnable = onEndRunnable;
+    }
+
+    public void setOnBitmapCropped(BitmapCropTask.OnBitmapCroppedHandler handler) {
+        mOnBitmapCroppedHandler = handler;
+    }
+
+    public void setNoCrop(boolean value) {
+        mNoCrop = value;
+    }
+
+    public void setOnEndRunnable(Runnable onEndRunnable) {
+        mOnEndRunnable = onEndRunnable;
+    }
+
+    // Helper to setup input stream
+    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 {
+            try {
+                if (mInUri != null) {
+                    return new BufferedInputStream(
+                            mContext.getContentResolver().openInputStream(mInUri));
+                } else if (mInFilePath != null) {
+                    return mContext.openFileInput(mInFilePath);
+                } else if (mInImageBytes != null) {
+                    return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
+                } else {
+                    return new BufferedInputStream(mResources.openRawResource(mInResId));
+                }
+            } catch (FileNotFoundException e) {
+                Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+            }
+        }
+        return null;
+    }
+
+    public Point getImageBounds() {
+        InputStream is = regenerateInputStream();
+        if (is != null) {
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inJustDecodeBounds = true;
+            BitmapFactory.decodeStream(is, null, options);
+            Utils.closeSilently(is);
+            if (options.outWidth != 0 && options.outHeight != 0) {
+                return new Point(options.outWidth, options.outHeight);
+            }
+        }
+        return null;
+    }
+
+    public void setCropBounds(RectF cropBounds) {
+        mCropBounds = cropBounds;
+    }
+
+    public Bitmap getCroppedBitmap() {
+        return mCroppedBitmap;
+    }
+    public boolean cropBitmap() {
+        boolean failure = false;
+
+
+        WallpaperManager wallpaperManager = null;
+        if (mSetWallpaper) {
+            wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
+        }
+
+
+        if (mSetWallpaper && mNoCrop) {
+            try {
+                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;
+        } else {
+            // Find crop bounds (scaled to original image size)
+            Rect roundedTrueCrop = new Rect();
+            Matrix rotateMatrix = new Matrix();
+            Matrix inverseRotateMatrix = new Matrix();
+
+            Point bounds = getImageBounds();
+            if (mRotation > 0) {
+                rotateMatrix.setRotate(mRotation);
+                inverseRotateMatrix.setRotate(-mRotation);
+
+                mCropBounds.roundOut(roundedTrueCrop);
+                mCropBounds = new RectF(roundedTrueCrop);
+
+                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);
+                rotatedBounds[0] = Math.abs(rotatedBounds[0]);
+                rotatedBounds[1] = Math.abs(rotatedBounds[1]);
+
+                mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
+                inverseRotateMatrix.mapRect(mCropBounds);
+                mCropBounds.offset(bounds.x/2, bounds.y/2);
+
+            }
+
+            mCropBounds.roundOut(roundedTrueCrop);
+
+            if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+                Log.w(LOGTAG, "crop has bad values for full size image");
+                failure = true;
+                return false;
+            }
+
+            // See how much we're reducing the size of the image
+            int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
+                    roundedTrueCrop.height() / mOutHeight));
+            // Attempt to open a region decoder
+            BitmapRegionDecoder decoder = null;
+            InputStream is = null;
+            try {
+                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);
+            } finally {
+               Utils.closeSilently(is);
+               is = null;
+            }
+
+            Bitmap crop = null;
+            if (decoder != null) {
+                // Do region decoding to get crop bitmap
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                if (scaleDownSampleSize > 1) {
+                    options.inSampleSize = scaleDownSampleSize;
+                }
+                crop = decoder.decodeRegion(roundedTrueCrop, options);
+                decoder.recycle();
+            }
+
+            if (crop == null) {
+                // BitmapRegionDecoder has failed, try to crop in-memory
+                is = regenerateInputStream();
+                Bitmap fullSize = null;
+                if (is != null) {
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    if (scaleDownSampleSize > 1) {
+                        options.inSampleSize = scaleDownSampleSize;
+                    }
+                    fullSize = BitmapFactory.decodeStream(is, null, options);
+                    Utils.closeSilently(is);
+                }
+                if (fullSize != null) {
+                    // Find out the true sample size that was used by the decoder
+                    scaleDownSampleSize = bounds.x / fullSize.getWidth();
+                    mCropBounds.left /= scaleDownSampleSize;
+                    mCropBounds.top /= scaleDownSampleSize;
+                    mCropBounds.bottom /= scaleDownSampleSize;
+                    mCropBounds.right /= scaleDownSampleSize;
+                    mCropBounds.roundOut(roundedTrueCrop);
+
+                    // Adjust values to account for issues related to rounding
+                    if (roundedTrueCrop.width() > fullSize.getWidth()) {
+                        // Adjust the width
+                        roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
+                    }
+                    if (roundedTrueCrop.right > fullSize.getWidth()) {
+                        // Adjust the left value
+                        int adjustment = roundedTrueCrop.left -
+                                Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width());
+                        roundedTrueCrop.left -= adjustment;
+                        roundedTrueCrop.right -= adjustment;
+                    }
+                    if (roundedTrueCrop.height() > fullSize.getHeight()) {
+                        // Adjust the height
+                        roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
+                    }
+                    if (roundedTrueCrop.bottom > fullSize.getHeight()) {
+                        // Adjust the top value
+                        int adjustment = roundedTrueCrop.top -
+                                Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height());
+                        roundedTrueCrop.top -= adjustment;
+                        roundedTrueCrop.bottom -= adjustment;
+                    }
+
+                    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 (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
+                float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
+                rotateMatrix.mapPoints(dimsAfter);
+                dimsAfter[0] = Math.abs(dimsAfter[0]);
+                dimsAfter[1] = Math.abs(dimsAfter[1]);
+
+                if (!(mOutWidth > 0 && mOutHeight > 0)) {
+                    mOutWidth = Math.round(dimsAfter[0]);
+                    mOutHeight = Math.round(dimsAfter[1]);
+                }
+
+                RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
+                RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
+
+                Matrix m = new Matrix();
+                if (mRotation == 0) {
+                    m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+                } else {
+                    Matrix m1 = new Matrix();
+                    m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
+                    Matrix m2 = new Matrix();
+                    m2.setRotate(mRotation);
+                    Matrix m3 = new Matrix();
+                    m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
+                    Matrix m4 = new Matrix();
+                    m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+
+                    Matrix c1 = new Matrix();
+                    c1.setConcat(m2, m1);
+                    Matrix c2 = new Matrix();
+                    c2.setConcat(m4, m3);
+                    m.setConcat(c2, c1);
+                }
+
+                Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+                        (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+                if (tmp != null) {
+                    Canvas c = new Canvas(tmp);
+                    Paint p = new Paint();
+                    p.setFilterBitmap(true);
+                    c.drawBitmap(crop, m, p);
+                    crop = tmp;
+                }
+            }
+
+            if (mSaveCroppedBitmap) {
+                mCroppedBitmap = crop;
+            }
+
+            // Compress to byte array
+            ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+            if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
+                // If we need to set to the wallpaper, set it
+                if (mSetWallpaper && wallpaperManager != null) {
+                    try {
+                        byte[] outByteArray = tmpOut.toByteArray();
+                        wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
+                        if (mOnBitmapCroppedHandler != null) {
+                            mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
+                        }
+                    } catch (IOException e) {
+                        Log.w(LOGTAG, "cannot write stream to wallpaper", e);
+                        failure = true;
+                    }
+                }
+            } else {
+                Log.w(LOGTAG, "cannot compress bitmap");
+                failure = true;
+            }
+        }
+        return !failure; // True if any of the operations failed
+    }
+
+    @Override
+    protected Boolean doInBackground(Void... params) {
+        return cropBitmap();
+    }
+
+    @Override
+    protected void onPostExecute(Boolean result) {
+        if (mOnEndRunnable != null) {
+            mOnEndRunnable.run();
+        }
+    }
+}
\ No newline at end of file
diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java
index 6a816d9..3470017 100644
--- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java
+++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java
@@ -16,83 +16,32 @@
 
 package com.android.gallery3d.common;
 
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
 import android.graphics.Canvas;
-import android.graphics.Matrix;
 import android.graphics.Paint;
-import android.util.FloatMath;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Build;
 import android.util.Log;
+import android.view.WindowManager;
 
-import java.io.ByteArrayOutputStream;
+import com.android.gallery3d.exif.ExifInterface;
+import com.android.launcher3.WallpaperCropActivity;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
 
 public class BitmapUtils {
+
     private static final String TAG = "BitmapUtils";
-    private static final int DEFAULT_JPEG_QUALITY = 90;
-    public static final int UNCONSTRAINED = -1;
-
-    private BitmapUtils(){}
-
-    /*
-     * Compute the sample size as a function of minSideLength
-     * and maxNumOfPixels.
-     * minSideLength is used to specify that minimal width or height of a
-     * bitmap.
-     * maxNumOfPixels is used to specify the maximal size in pixels that is
-     * tolerable in terms of memory usage.
-     *
-     * The function returns a sample size based on the constraints.
-     * Both size and minSideLength can be passed in as UNCONSTRAINED,
-     * which indicates no care of the corresponding constraint.
-     * The functions prefers returning a sample size that
-     * generates a smaller bitmap, unless minSideLength = UNCONSTRAINED.
-     *
-     * Also, the function rounds up the sample size to a power of 2 or multiple
-     * of 8 because BitmapFactory only honors sample size this way.
-     * For example, BitmapFactory downsamples an image by 2 even though the
-     * request is 3. So we round up the sample size to avoid OOM.
-     */
-    public static int computeSampleSize(int width, int height,
-            int minSideLength, int maxNumOfPixels) {
-        int initialSize = computeInitialSampleSize(
-                width, height, minSideLength, maxNumOfPixels);
-
-        return initialSize <= 8
-                ? Utils.nextPowerOf2(initialSize)
-                : (initialSize + 7) / 8 * 8;
-    }
-
-    private static int computeInitialSampleSize(int w, int h,
-            int minSideLength, int maxNumOfPixels) {
-        if (maxNumOfPixels == UNCONSTRAINED
-                && minSideLength == UNCONSTRAINED) return 1;
-
-        int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
-                (int) FloatMath.ceil(FloatMath.sqrt((float) (w * h) / maxNumOfPixels));
-
-        if (minSideLength == UNCONSTRAINED) {
-            return lowerBound;
-        } else {
-            int sampleSize = Math.min(w / minSideLength, h / minSideLength);
-            return Math.max(sampleSize, lowerBound);
-        }
-    }
-
-    // This computes a sample size which makes the longer side at least
-    // minSideLength long. If that's not possible, return 1.
-    public static int computeSampleSizeLarger(int w, int h,
-            int minSideLength) {
-        int initialSize = Math.max(w / minSideLength, h / minSideLength);
-        if (initialSize <= 1) return 1;
-
-        return initialSize <= 8
-                ? Utils.prevPowerOf2(initialSize)
-                : initialSize / 8 * 8;
-    }
 
     // Find the min x that 1 / x >= scale
     public static int computeSampleSizeLarger(float scale) {
-        int initialSize = (int) FloatMath.floor(1f / scale);
+        int initialSize = (int) Math.floor(1f / scale);
         if (initialSize <= 1) return 1;
 
         return initialSize <= 8
@@ -100,15 +49,6 @@
                 : initialSize / 8 * 8;
     }
 
-    // Find the max x that 1 / x <= scale.
-    public static int computeSampleSize(float scale) {
-        Utils.assertTrue(scale > 0);
-        int initialSize = Math.max(1, (int) FloatMath.ceil(1 / scale));
-        return initialSize <= 8
-                ? Utils.nextPowerOf2(initialSize)
-                : (initialSize + 7) / 8 * 8;
-    }
-
     public static Bitmap resizeBitmapByScale(
             Bitmap bitmap, float scale, boolean recycle) {
         int width = Math.round(bitmap.getWidth() * scale);
@@ -132,77 +72,104 @@
         return config;
     }
 
-    public static Bitmap resizeDownBySideLength(
-            Bitmap bitmap, int maxLength, boolean recycle) {
-        int srcWidth = bitmap.getWidth();
-        int srcHeight = bitmap.getHeight();
-        float scale = Math.min(
-                (float) maxLength / srcWidth, (float) maxLength / srcHeight);
-        if (scale >= 1.0f) return bitmap;
-        return resizeBitmapByScale(bitmap, scale, recycle);
+    /**
+     * As a ratio of screen height, the total distance we want the parallax effect to span
+     * horizontally
+     */
+    public static float wallpaperTravelToScreenWidthRatio(int width, int height) {
+        float aspectRatio = width / (float) height;
+
+        // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
+        // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
+        // We will use these two data points to extrapolate how much the wallpaper parallax effect
+        // to span (ie travel) at any aspect ratio:
+
+        final float ASPECT_RATIO_LANDSCAPE = 16/10f;
+        final float ASPECT_RATIO_PORTRAIT = 10/16f;
+        final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
+        final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
+
+        // To find out the desired width at different aspect ratios, we use the following two
+        // formulas, where the coefficient on x is the aspect ratio (width/height):
+        //   (16/10)x + y = 1.5
+        //   (10/16)x + y = 1.2
+        // We solve for x and y and end up with a final formula:
+        final float x =
+            (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
+            (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
+        final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
+        return x * aspectRatio + y;
     }
 
-    public static Bitmap resizeAndCropCenter(Bitmap bitmap, int size, boolean recycle) {
-        int w = bitmap.getWidth();
-        int h = bitmap.getHeight();
-        if (w == size && h == size) return bitmap;
+    private static Point sDefaultWallpaperSize;
 
-        // scale the image so that the shorter side equals to the target;
-        // the longer side will be center-cropped.
-        float scale = (float) size / Math.min(w,  h);
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+    public static Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
+        if (sDefaultWallpaperSize == null) {
+            Point minDims = new Point();
+            Point maxDims = new Point();
+            windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
 
-        Bitmap target = Bitmap.createBitmap(size, size, getConfig(bitmap));
-        int width = Math.round(scale * bitmap.getWidth());
-        int height = Math.round(scale * bitmap.getHeight());
-        Canvas canvas = new Canvas(target);
-        canvas.translate((size - width) / 2f, (size - height) / 2f);
-        canvas.scale(scale, scale);
-        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
-        canvas.drawBitmap(bitmap, 0, 0, paint);
-        if (recycle) bitmap.recycle();
-        return target;
-    }
+            int maxDim = Math.max(maxDims.x, maxDims.y);
+            int minDim = Math.max(minDims.x, minDims.y);
 
-    public static void recycleSilently(Bitmap bitmap) {
-        if (bitmap == null) return;
-        try {
-            bitmap.recycle();
-        } catch (Throwable t) {
-            Log.w(TAG, "unable recycle bitmap", t);
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                Point realSize = new Point();
+                windowManager.getDefaultDisplay().getRealSize(realSize);
+                maxDim = Math.max(realSize.x, realSize.y);
+                minDim = Math.min(realSize.x, realSize.y);
+            }
+
+            // We need to ensure that there is enough extra space in the wallpaper
+            // for the intended parallax effects
+            final int defaultWidth, defaultHeight;
+            if (res.getConfiguration().smallestScreenWidthDp >= 720) {
+                defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
+                defaultHeight = maxDim;
+            } else {
+                defaultWidth = Math.max((int) (minDim * WallpaperCropActivity.WALLPAPER_SCREENS_SPAN), maxDim);
+                defaultHeight = maxDim;
+            }
+            sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight);
         }
+        return sDefaultWallpaperSize;
     }
 
-    public static Bitmap rotateBitmap(Bitmap source, int rotation, boolean recycle) {
-        if (rotation == 0) return source;
-        int w = source.getWidth();
-        int h = source.getHeight();
-        Matrix m = new Matrix();
-        m.postRotate(rotation);
-        Bitmap bitmap = Bitmap.createBitmap(source, 0, 0, w, h, m, true);
-        if (recycle) source.recycle();
-        return bitmap;
+    public static int getRotationFromExif(Context context, Uri uri) {
+        return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri);
     }
 
-    public static byte[] compressToBytes(Bitmap bitmap) {
-        return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
+    public static int getRotationFromExif(Resources res, int resId) {
+        return BitmapUtils.getRotationFromExifHelper(res, resId, null, null);
     }
 
-    public static byte[] compressToBytes(Bitmap bitmap, int quality) {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
-        bitmap.compress(CompressFormat.JPEG, quality, baos);
-        return baos.toByteArray();
-    }
-
-    public static boolean isSupportedByRegionDecoder(String mimeType) {
-        if (mimeType == null) return false;
-        mimeType = mimeType.toLowerCase();
-        return mimeType.startsWith("image/") &&
-                (!mimeType.equals("image/gif") && !mimeType.endsWith("bmp"));
-    }
-
-    public static boolean isRotationSupported(String mimeType) {
-        if (mimeType == null) return false;
-        mimeType = mimeType.toLowerCase();
-        return mimeType.equals("image/jpeg");
+    private static int getRotationFromExifHelper(Resources res, int resId, Context context, Uri uri) {
+        ExifInterface ei = new ExifInterface();
+        InputStream is = null;
+        BufferedInputStream bis = null;
+        try {
+            if (uri != null) {
+                is = context.getContentResolver().openInputStream(uri);
+                bis = new BufferedInputStream(is);
+                ei.readExif(bis);
+            } else {
+                is = res.openRawResource(resId);
+                bis = new BufferedInputStream(is);
+                ei.readExif(bis);
+            }
+            Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+            if (ori != null) {
+                return ExifInterface.getRotationForOrientationValue(ori.shortValue());
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Getting exif data failed", e);
+        } catch (NullPointerException e) {
+            // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid
+            Log.w(TAG, "Getting exif data failed", e);
+        } finally {
+            Utils.closeSilently(bis);
+            Utils.closeSilently(is);
+        }
+        return 0;
     }
 }
diff --git a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java
index 614a081..8466c22 100644
--- a/WallpaperPicker/src/com/android/gallery3d/common/Utils.java
+++ b/WallpaperPicker/src/com/android/gallery3d/common/Utils.java
@@ -16,32 +16,16 @@
 
 package com.android.gallery3d.common;
 
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.Cursor;
-import android.os.Build;
+import android.graphics.RectF;
 import android.os.ParcelFileDescriptor;
-import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.Closeable;
 import java.io.IOException;
-import java.io.InterruptedIOException;
 
 public class Utils {
     private static final String TAG = "Utils";
-    private static final String DEBUG_TAG = "GalleryDebug";
-
-    private static final long POLY64REV = 0x95AC9329AC4BC9B5L;
-    private static final long INITIALCRC = 0xFFFFFFFFFFFFFFFFL;
-
-    private static long[] sCrcTable = new long[256];
-
-    private static final boolean IS_DEBUG_BUILD =
-            Build.TYPE.equals("eng") || Build.TYPE.equals("userdebug");
-
-    private static final String MASK_STRING = "********************************";
 
     // Throws AssertionError if the input is false.
     public static void assertTrue(boolean cond) {
@@ -50,28 +34,6 @@
         }
     }
 
-    // Throws AssertionError with the message. We had a method having the form
-    //   assertTrue(boolean cond, String message, Object ... args);
-    // However a call to that method will cause memory allocation even if the
-    // condition is false (due to autoboxing generated by "Object ... args"),
-    // so we don't use that anymore.
-    public static void fail(String message, Object ... args) {
-        throw new AssertionError(
-                args.length == 0 ? message : String.format(message, args));
-    }
-
-    // Throws NullPointerException if the input is null.
-    public static <T> T checkNotNull(T object) {
-        if (object == null) throw new NullPointerException();
-        return object;
-    }
-
-    // Returns true if two input Object are both null or equal
-    // to each other.
-    public static boolean equals(Object a, Object b) {
-        return (a == b) || (a == null ? false : a.equals(b));
-    }
-
     // Returns the next power of two.
     // Returns the input if it is already power of 2.
     // Throws IllegalArgumentException if the input is <= 0 or
@@ -102,87 +64,6 @@
         return x;
     }
 
-    // Returns the input value x clamped to the range [min, max].
-    public static float clamp(float x, float min, float max) {
-        if (x > max) return max;
-        if (x < min) return min;
-        return x;
-    }
-
-    // Returns the input value x clamped to the range [min, max].
-    public static long clamp(long x, long min, long max) {
-        if (x > max) return max;
-        if (x < min) return min;
-        return x;
-    }
-
-    public static boolean isOpaque(int color) {
-        return color >>> 24 == 0xFF;
-    }
-
-    public static void swap(int[] array, int i, int j) {
-        int temp = array[i];
-        array[i] = array[j];
-        array[j] = temp;
-    }
-
-    /**
-     * A function thats returns a 64-bit crc for string
-     *
-     * @param in input string
-     * @return a 64-bit crc value
-     */
-    public static final long crc64Long(String in) {
-        if (in == null || in.length() == 0) {
-            return 0;
-        }
-        return crc64Long(getBytes(in));
-    }
-
-    static {
-        // http://bioinf.cs.ucl.ac.uk/downloads/crc64/crc64.c
-        long part;
-        for (int i = 0; i < 256; i++) {
-            part = i;
-            for (int j = 0; j < 8; j++) {
-                long x = ((int) part & 1) != 0 ? POLY64REV : 0;
-                part = (part >> 1) ^ x;
-            }
-            sCrcTable[i] = part;
-        }
-    }
-
-    public static final long crc64Long(byte[] buffer) {
-        long crc = INITIALCRC;
-        for (int k = 0, n = buffer.length; k < n; ++k) {
-            crc = sCrcTable[(((int) crc) ^ buffer[k]) & 0xff] ^ (crc >> 8);
-        }
-        return crc;
-    }
-
-    public static byte[] getBytes(String in) {
-        byte[] result = new byte[in.length() * 2];
-        int output = 0;
-        for (char ch : in.toCharArray()) {
-            result[output++] = (byte) (ch & 0xFF);
-            result[output++] = (byte) (ch >> 8);
-        }
-        return result;
-    }
-
-    public static void closeSilently(Closeable c) {
-        if (c == null) return;
-        try {
-            c.close();
-        } catch (IOException t) {
-            Log.w(TAG, "close fail ", t);
-        }
-    }
-
-    public static int compare(long a, long b) {
-        return a < b ? -1 : a == b ? 0 : 1;
-    }
-
     public static int ceilLog2(float value) {
         int i;
         for (i = 0; i < 31; i++) {
@@ -199,6 +80,15 @@
         return i - 1;
     }
 
+    public static void closeSilently(Closeable c) {
+        if (c == null) return;
+        try {
+            c.close();
+        } catch (IOException t) {
+            Log.w(TAG, "close fail ", t);
+        }
+    }
+
     public static void closeSilently(ParcelFileDescriptor fd) {
         try {
             if (fd != null) fd.close();
@@ -215,126 +105,25 @@
         }
     }
 
-    public static float interpolateAngle(
-            float source, float target, float progress) {
-        // interpolate the angle from source to target
-        // We make the difference in the range of [-179, 180], this is the
-        // shortest path to change source to target.
-        float diff = target - source;
-        if (diff < 0) diff += 360f;
-        if (diff > 180) diff -= 360f;
-
-        float result = source + diff * progress;
-        return result < 0 ? result + 360f : result;
-    }
-
-    public static float interpolateScale(
-            float source, float target, float progress) {
-        return source + progress * (target - source);
-    }
-
-    public static String ensureNotNull(String value) {
-        return value == null ? "" : value;
-    }
-
-    public static float parseFloatSafely(String content, float defaultValue) {
-        if (content == null) return defaultValue;
-        try {
-            return Float.parseFloat(content);
-        } catch (NumberFormatException e) {
-            return defaultValue;
+    public static RectF getMaxCropRect(
+            int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
+        RectF cropRect = new RectF();
+        // Get a crop rect that will fit this
+        if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
+             cropRect.top = 0;
+             cropRect.bottom = inHeight;
+             cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
+             cropRect.right = inWidth - cropRect.left;
+             if (leftAligned) {
+                 cropRect.right -= cropRect.left;
+                 cropRect.left = 0;
+             }
+        } else {
+            cropRect.left = 0;
+            cropRect.right = inWidth;
+            cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
+            cropRect.bottom = inHeight - cropRect.top;
         }
-    }
-
-    public static int parseIntSafely(String content, int defaultValue) {
-        if (content == null) return defaultValue;
-        try {
-            return Integer.parseInt(content);
-        } catch (NumberFormatException e) {
-            return defaultValue;
-        }
-    }
-
-    public static boolean isNullOrEmpty(String exifMake) {
-        return TextUtils.isEmpty(exifMake);
-    }
-
-    public static void waitWithoutInterrupt(Object object) {
-        try {
-            object.wait();
-        } catch (InterruptedException e) {
-            Log.w(TAG, "unexpected interrupt: " + object);
-        }
-    }
-
-    public static boolean handleInterrruptedException(Throwable e) {
-        // A helper to deal with the interrupt exception
-        // If an interrupt detected, we will setup the bit again.
-        if (e instanceof InterruptedIOException
-                || e instanceof InterruptedException) {
-            Thread.currentThread().interrupt();
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * @return String with special XML characters escaped.
-     */
-    public static String escapeXml(String s) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0, len = s.length(); i < len; ++i) {
-            char c = s.charAt(i);
-            switch (c) {
-                case '<':  sb.append("&lt;"); break;
-                case '>':  sb.append("&gt;"); break;
-                case '\"': sb.append("&quot;"); break;
-                case '\'': sb.append("&#039;"); break;
-                case '&':  sb.append("&amp;"); break;
-                default: sb.append(c);
-            }
-        }
-        return sb.toString();
-    }
-
-    public static String getUserAgent(Context context) {
-        PackageInfo packageInfo;
-        try {
-            packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
-        } catch (NameNotFoundException e) {
-            throw new IllegalStateException("getPackageInfo failed");
-        }
-        return String.format("%s/%s; %s/%s/%s/%s; %s/%s/%s",
-                packageInfo.packageName,
-                packageInfo.versionName,
-                Build.BRAND,
-                Build.DEVICE,
-                Build.MODEL,
-                Build.ID,
-                Build.VERSION.SDK_INT,
-                Build.VERSION.RELEASE,
-                Build.VERSION.INCREMENTAL);
-    }
-
-    public static String[] copyOf(String[] source, int newSize) {
-        String[] result = new String[newSize];
-        newSize = Math.min(source.length, newSize);
-        System.arraycopy(source, 0, result, 0, newSize);
-        return result;
-    }
-
-    // Mask information for debugging only. It returns <code>info.toString()</code> directly
-    // for debugging build (i.e., 'eng' and 'userdebug') and returns a mask ("****")
-    // in release build to protect the information (e.g. for privacy issue).
-    public static String maskDebugInfo(Object info) {
-        if (info == null) return null;
-        String s = info.toString();
-        int length = Math.min(s.length(), MASK_STRING.length());
-        return IS_DEBUG_BUILD ? s : MASK_STRING.substring(0, length);
-    }
-
-    // This method should be ONLY used for debugging.
-    public static void debug(String message, Object ... args) {
-        Log.v(DEBUG_TAG, String.format(message, args));
+        return cropRect;
     }
 }
diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java
index 2e77b90..0f3efb7 100644
--- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java
+++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/BasicTexture.java
@@ -27,7 +27,6 @@
 // If a BasicTexture is loaded into GL memory, it has a GL texture id.
 public abstract class BasicTexture implements Texture {
 
-    @SuppressWarnings("unused")
     private static final String TAG = "BasicTexture";
     protected static final int UNSPECIFIED = -1;
 
diff --git a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
index 4ead131..8af1f59 100644
--- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
+++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/GLES20Canvas.java
@@ -23,8 +23,6 @@
 import android.opengl.Matrix;
 import android.util.Log;
 
-import com.android.gallery3d.util.IntArray;
-
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
diff --git a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java
similarity index 97%
rename from WallpaperPicker/src/com/android/gallery3d/util/IntArray.java
rename to WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java
index 2c4dc2c..f123624 100644
--- a/WallpaperPicker/src/com/android/gallery3d/util/IntArray.java
+++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/IntArray.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.gallery3d.util;
+package com.android.gallery3d.glrenderer;
 
 public class IntArray {
     private static final int INIT_CAPACITY = 8;
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
index fa8ec64..71c7a16 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.annotation.TargetApi;
 import android.app.ActionBar;
 import android.app.Activity;
 import android.app.WallpaperManager;
@@ -24,43 +25,30 @@
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-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.Point;
-import android.graphics.Rect;
 import android.graphics.RectF;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Bundle;
 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.BitmapCropTask;
+import com.android.gallery3d.common.BitmapUtils;
 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;
-import java.io.ByteArrayOutputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-
 public class WallpaperCropActivity extends Activity {
     private static final String LOGTAG = "Launcher3.CropActivity";
 
     protected static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
     protected static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
-    private static final int DEFAULT_COMPRESS_QUALITY = 90;
+
     /**
      * 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
@@ -69,9 +57,7 @@
      * array instead of a Bitmap instance to avoid overhead.
      */
     public static final int MAX_BMAP_IN_INTENT = 750000;
-    private static final float WALLPAPER_SCREENS_SPAN = 2f;
-
-    protected static Point sDefaultWallpaperSize;
+    public static final float WALLPAPER_SCREENS_SPAN = 2f;
 
     protected CropView mCropView;
     protected Uri mUri;
@@ -205,111 +191,8 @@
         return LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY;
     }
 
-    // As a ratio of screen height, the total distance we want the parallax effect to span
-    // horizontally
-    private static float wallpaperTravelToScreenWidthRatio(int width, int height) {
-        float aspectRatio = width / (float) height;
-
-        // At an aspect ratio of 16/10, the wallpaper parallax effect should span 1.5 * screen width
-        // At an aspect ratio of 10/16, the wallpaper parallax effect should span 1.2 * screen width
-        // We will use these two data points to extrapolate how much the wallpaper parallax effect
-        // to span (ie travel) at any aspect ratio:
-
-        final float ASPECT_RATIO_LANDSCAPE = 16/10f;
-        final float ASPECT_RATIO_PORTRAIT = 10/16f;
-        final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE = 1.5f;
-        final float WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT = 1.2f;
-
-        // To find out the desired width at different aspect ratios, we use the following two
-        // formulas, where the coefficient on x is the aspect ratio (width/height):
-        //   (16/10)x + y = 1.5
-        //   (10/16)x + y = 1.2
-        // We solve for x and y and end up with a final formula:
-        final float x =
-            (WALLPAPER_WIDTH_TO_SCREEN_RATIO_LANDSCAPE - WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT) /
-            (ASPECT_RATIO_LANDSCAPE - ASPECT_RATIO_PORTRAIT);
-        final float y = WALLPAPER_WIDTH_TO_SCREEN_RATIO_PORTRAIT - x * ASPECT_RATIO_PORTRAIT;
-        return x * aspectRatio + y;
-    }
-
-    static protected Point getDefaultWallpaperSize(Resources res, WindowManager windowManager) {
-        if (sDefaultWallpaperSize == null) {
-            Point minDims = new Point();
-            Point maxDims = new Point();
-            windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
-
-            int maxDim = Math.max(maxDims.x, maxDims.y);
-            int minDim = Math.max(minDims.x, minDims.y);
-
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
-                Point realSize = new Point();
-                windowManager.getDefaultDisplay().getRealSize(realSize);
-                maxDim = Math.max(realSize.x, realSize.y);
-                minDim = Math.min(realSize.x, realSize.y);
-            }
-
-            // We need to ensure that there is enough extra space in the wallpaper
-            // for the intended parallax effects
-            final int defaultWidth, defaultHeight;
-            if (isScreenLarge(res)) {
-                defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
-                defaultHeight = maxDim;
-            } else {
-                defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
-                defaultHeight = maxDim;
-            }
-            sDefaultWallpaperSize = new Point(defaultWidth, defaultHeight);
-        }
-        return sDefaultWallpaperSize;
-    }
-
-    public static int getRotationFromExif(String path) {
-        return getRotationFromExifHelper(path, null, 0, null, null);
-    }
-
-    public static int getRotationFromExif(Context context, Uri uri) {
-        return getRotationFromExifHelper(null, null, 0, context, uri);
-    }
-
-    public static int getRotationFromExif(Resources res, int resId) {
-        return getRotationFromExifHelper(null, res, resId, null, null);
-    }
-
-    private static int getRotationFromExifHelper(
-            String path, Resources res, int resId, Context context, Uri uri) {
-        ExifInterface ei = new ExifInterface();
-        InputStream is = null;
-        BufferedInputStream bis = null;
-        try {
-            if (path != null) {
-                ei.readExif(path);
-            } else if (uri != null) {
-                is = context.getContentResolver().openInputStream(uri);
-                bis = new BufferedInputStream(is);
-                ei.readExif(bis);
-            } else {
-                is = res.openRawResource(resId);
-                bis = new BufferedInputStream(is);
-                ei.readExif(bis);
-            }
-            Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
-            if (ori != null) {
-                return ExifInterface.getRotationForOrientationValue(ori.shortValue());
-            }
-        } catch (IOException e) {
-            Log.w(LOGTAG, "Getting exif data failed", e);
-        } catch (NullPointerException e) {
-            // Sometimes the ExifInterface has an internal NPE if Exif data isn't valid
-            Log.w(LOGTAG, "Getting exif data failed", e);
-        } finally {
-            Utils.closeSilently(bis);
-            Utils.closeSilently(is);
-        }
-        return 0;
-    }
-
     protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) {
-        int rotation = getRotationFromExif(this, uri);
+        int rotation = BitmapUtils.getRotationFromExif(this, uri);
         BitmapCropTask cropTask = new BitmapCropTask(
                 this, uri, null, rotation, 0, 0, true, false, null);
         final Point bounds = cropTask.getImageBounds();
@@ -331,11 +214,11 @@
             Resources res, int resId, final boolean finishActivityWhenDone) {
         // crop this image and scale it down to the default wallpaper size for
         // this device
-        int rotation = getRotationFromExif(res, resId);
+        int rotation = BitmapUtils.getRotationFromExif(res, resId);
         Point inSize = mCropView.getSourceDimensions();
-        Point outSize = getDefaultWallpaperSize(getResources(),
+        Point outSize = BitmapUtils.getDefaultWallpaperSize(getResources(),
                 getWindowManager());
-        RectF crop = getMaxCropRect(
+        RectF crop = Utils.getMaxCropRect(
                 inSize.x, inSize.y, outSize.x, outSize.y, false);
         Runnable onEndCrop = new Runnable() {
             public void run() {
@@ -353,13 +236,9 @@
         cropTask.execute();
     }
 
-    private static boolean isScreenLarge(Resources res) {
-        Configuration config = res.getConfiguration();
-        return config.smallestScreenWidthDp >= 720;
-    }
-
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
     protected void cropImageAndSetWallpaper(Uri uri,
-            OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
+            BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler, final boolean finishActivityWhenDone) {
         boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
         // Get the crop
         boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
@@ -370,7 +249,7 @@
         d.getSize(displaySize);
         boolean isPortrait = displaySize.x < displaySize.y;
 
-        Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(),
+        Point defaultWallpaperSize = BitmapUtils.getDefaultWallpaperSize(getResources(),
                 getWindowManager());
         // Get the crop
         RectF cropRect = mCropView.getCrop();
@@ -452,372 +331,6 @@
         cropTask.execute();
     }
 
-    public interface OnBitmapCroppedHandler {
-        public void onBitmapCropped(byte[] imageBytes);
-    }
-
-    protected static class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
-        Uri mInUri = null;
-        Context mContext;
-        String mInFilePath;
-        byte[] mInImageBytes;
-        int mInResId = 0;
-        RectF mCropBounds = null;
-        int mOutWidth, mOutHeight;
-        int mRotation;
-        String mOutputFormat = "jpg"; // for now
-        boolean mSetWallpaper;
-        boolean mSaveCroppedBitmap;
-        Bitmap mCroppedBitmap;
-        Runnable mOnEndRunnable;
-        Resources mResources;
-        OnBitmapCroppedHandler mOnBitmapCroppedHandler;
-        boolean mNoCrop;
-
-        public BitmapCropTask(Context c, String filePath,
-                RectF cropBounds, int rotation, int outWidth, int outHeight,
-                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
-            mContext = c;
-            mInFilePath = filePath;
-            init(cropBounds, rotation,
-                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
-        }
-
-        public BitmapCropTask(byte[] imageBytes,
-                RectF cropBounds, int rotation, int outWidth, int outHeight,
-                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
-            mInImageBytes = imageBytes;
-            init(cropBounds, rotation,
-                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
-        }
-
-        public BitmapCropTask(Context c, Uri inUri,
-                RectF cropBounds, int rotation, int outWidth, int outHeight,
-                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
-            mContext = c;
-            mInUri = inUri;
-            init(cropBounds, rotation,
-                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
-        }
-
-        public BitmapCropTask(Context c, Resources res, int inResId,
-                RectF cropBounds, int rotation, int outWidth, int outHeight,
-                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
-            mContext = c;
-            mInResId = inResId;
-            mResources = res;
-            init(cropBounds, rotation,
-                    outWidth, outHeight, setWallpaper, saveCroppedBitmap, onEndRunnable);
-        }
-
-        private void init(RectF cropBounds, int rotation, int outWidth, int outHeight,
-                boolean setWallpaper, boolean saveCroppedBitmap, Runnable onEndRunnable) {
-            mCropBounds = cropBounds;
-            mRotation = rotation;
-            mOutWidth = outWidth;
-            mOutHeight = outHeight;
-            mSetWallpaper = setWallpaper;
-            mSaveCroppedBitmap = saveCroppedBitmap;
-            mOnEndRunnable = onEndRunnable;
-        }
-
-        public void setOnBitmapCropped(OnBitmapCroppedHandler handler) {
-            mOnBitmapCroppedHandler = handler;
-        }
-
-        public void setNoCrop(boolean value) {
-            mNoCrop = value;
-        }
-
-        public void setOnEndRunnable(Runnable onEndRunnable) {
-            mOnEndRunnable = onEndRunnable;
-        }
-
-        // Helper to setup input stream
-        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 {
-                try {
-                    if (mInUri != null) {
-                        return new BufferedInputStream(
-                                mContext.getContentResolver().openInputStream(mInUri));
-                    } else if (mInFilePath != null) {
-                        return mContext.openFileInput(mInFilePath);
-                    } else if (mInImageBytes != null) {
-                        return new BufferedInputStream(new ByteArrayInputStream(mInImageBytes));
-                    } else {
-                        return new BufferedInputStream(mResources.openRawResource(mInResId));
-                    }
-                } catch (FileNotFoundException e) {
-                    Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
-                }
-            }
-            return null;
-        }
-
-        public Point getImageBounds() {
-            InputStream is = regenerateInputStream();
-            if (is != null) {
-                BitmapFactory.Options options = new BitmapFactory.Options();
-                options.inJustDecodeBounds = true;
-                BitmapFactory.decodeStream(is, null, options);
-                Utils.closeSilently(is);
-                if (options.outWidth != 0 && options.outHeight != 0) {
-                    return new Point(options.outWidth, options.outHeight);
-                }
-            }
-            return null;
-        }
-
-        public void setCropBounds(RectF cropBounds) {
-            mCropBounds = cropBounds;
-        }
-
-        public Bitmap getCroppedBitmap() {
-            return mCroppedBitmap;
-        }
-        public boolean cropBitmap() {
-            boolean failure = false;
-
-
-            WallpaperManager wallpaperManager = null;
-            if (mSetWallpaper) {
-                wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
-            }
-
-
-            if (mSetWallpaper && mNoCrop) {
-                try {
-                    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;
-            } else {
-                // Find crop bounds (scaled to original image size)
-                Rect roundedTrueCrop = new Rect();
-                Matrix rotateMatrix = new Matrix();
-                Matrix inverseRotateMatrix = new Matrix();
-
-                Point bounds = getImageBounds();
-                if (mRotation > 0) {
-                    rotateMatrix.setRotate(mRotation);
-                    inverseRotateMatrix.setRotate(-mRotation);
-
-                    mCropBounds.roundOut(roundedTrueCrop);
-                    mCropBounds = new RectF(roundedTrueCrop);
-
-                    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);
-                    rotatedBounds[0] = Math.abs(rotatedBounds[0]);
-                    rotatedBounds[1] = Math.abs(rotatedBounds[1]);
-
-                    mCropBounds.offset(-rotatedBounds[0]/2, -rotatedBounds[1]/2);
-                    inverseRotateMatrix.mapRect(mCropBounds);
-                    mCropBounds.offset(bounds.x/2, bounds.y/2);
-
-                }
-
-                mCropBounds.roundOut(roundedTrueCrop);
-
-                if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
-                    Log.w(LOGTAG, "crop has bad values for full size image");
-                    failure = true;
-                    return false;
-                }
-
-                // See how much we're reducing the size of the image
-                int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
-                        roundedTrueCrop.height() / mOutHeight));
-                // Attempt to open a region decoder
-                BitmapRegionDecoder decoder = null;
-                InputStream is = null;
-                try {
-                    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);
-                } finally {
-                   Utils.closeSilently(is);
-                   is = null;
-                }
-
-                Bitmap crop = null;
-                if (decoder != null) {
-                    // Do region decoding to get crop bitmap
-                    BitmapFactory.Options options = new BitmapFactory.Options();
-                    if (scaleDownSampleSize > 1) {
-                        options.inSampleSize = scaleDownSampleSize;
-                    }
-                    crop = decoder.decodeRegion(roundedTrueCrop, options);
-                    decoder.recycle();
-                }
-
-                if (crop == null) {
-                    // BitmapRegionDecoder has failed, try to crop in-memory
-                    is = regenerateInputStream();
-                    Bitmap fullSize = null;
-                    if (is != null) {
-                        BitmapFactory.Options options = new BitmapFactory.Options();
-                        if (scaleDownSampleSize > 1) {
-                            options.inSampleSize = scaleDownSampleSize;
-                        }
-                        fullSize = BitmapFactory.decodeStream(is, null, options);
-                        Utils.closeSilently(is);
-                    }
-                    if (fullSize != null) {
-                        // Find out the true sample size that was used by the decoder
-                        scaleDownSampleSize = bounds.x / fullSize.getWidth();
-                        mCropBounds.left /= scaleDownSampleSize;
-                        mCropBounds.top /= scaleDownSampleSize;
-                        mCropBounds.bottom /= scaleDownSampleSize;
-                        mCropBounds.right /= scaleDownSampleSize;
-                        mCropBounds.roundOut(roundedTrueCrop);
-
-                        // Adjust values to account for issues related to rounding
-                        if (roundedTrueCrop.width() > fullSize.getWidth()) {
-                            // Adjust the width
-                            roundedTrueCrop.right = roundedTrueCrop.left + fullSize.getWidth();
-                        }
-                        if (roundedTrueCrop.right > fullSize.getWidth()) {
-                            // Adjust the left value
-                            int adjustment = roundedTrueCrop.left -
-                                    Math.max(0, roundedTrueCrop.right - roundedTrueCrop.width());
-                            roundedTrueCrop.left -= adjustment;
-                            roundedTrueCrop.right -= adjustment;
-                        }
-                        if (roundedTrueCrop.height() > fullSize.getHeight()) {
-                            // Adjust the height
-                            roundedTrueCrop.bottom = roundedTrueCrop.top + fullSize.getHeight();
-                        }
-                        if (roundedTrueCrop.bottom > fullSize.getHeight()) {
-                            // Adjust the top value
-                            int adjustment = roundedTrueCrop.top -
-                                    Math.max(0, roundedTrueCrop.bottom - roundedTrueCrop.height());
-                            roundedTrueCrop.top -= adjustment;
-                            roundedTrueCrop.bottom -= adjustment;
-                        }
-
-                        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 (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
-                    float[] dimsAfter = new float[] { crop.getWidth(), crop.getHeight() };
-                    rotateMatrix.mapPoints(dimsAfter);
-                    dimsAfter[0] = Math.abs(dimsAfter[0]);
-                    dimsAfter[1] = Math.abs(dimsAfter[1]);
-
-                    if (!(mOutWidth > 0 && mOutHeight > 0)) {
-                        mOutWidth = Math.round(dimsAfter[0]);
-                        mOutHeight = Math.round(dimsAfter[1]);
-                    }
-
-                    RectF cropRect = new RectF(0, 0, dimsAfter[0], dimsAfter[1]);
-                    RectF returnRect = new RectF(0, 0, mOutWidth, mOutHeight);
-
-                    Matrix m = new Matrix();
-                    if (mRotation == 0) {
-                        m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
-                    } else {
-                        Matrix m1 = new Matrix();
-                        m1.setTranslate(-crop.getWidth() / 2f, -crop.getHeight() / 2f);
-                        Matrix m2 = new Matrix();
-                        m2.setRotate(mRotation);
-                        Matrix m3 = new Matrix();
-                        m3.setTranslate(dimsAfter[0] / 2f, dimsAfter[1] / 2f);
-                        Matrix m4 = new Matrix();
-                        m4.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
-
-                        Matrix c1 = new Matrix();
-                        c1.setConcat(m2, m1);
-                        Matrix c2 = new Matrix();
-                        c2.setConcat(m4, m3);
-                        m.setConcat(c2, c1);
-                    }
-
-                    Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
-                            (int) returnRect.height(), Bitmap.Config.ARGB_8888);
-                    if (tmp != null) {
-                        Canvas c = new Canvas(tmp);
-                        Paint p = new Paint();
-                        p.setFilterBitmap(true);
-                        c.drawBitmap(crop, m, p);
-                        crop = tmp;
-                    }
-                }
-
-                if (mSaveCroppedBitmap) {
-                    mCroppedBitmap = crop;
-                }
-
-                // Get output compression format
-                CompressFormat cf =
-                        convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
-
-                // Compress to byte array
-                ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
-                if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
-                    // If we need to set to the wallpaper, set it
-                    if (mSetWallpaper && wallpaperManager != null) {
-                        try {
-                            byte[] outByteArray = tmpOut.toByteArray();
-                            wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
-                            if (mOnBitmapCroppedHandler != null) {
-                                mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
-                            }
-                        } catch (IOException e) {
-                            Log.w(LOGTAG, "cannot write stream to wallpaper", e);
-                            failure = true;
-                        }
-                    }
-                } else {
-                    Log.w(LOGTAG, "cannot compress bitmap");
-                    failure = true;
-                }
-            }
-            return !failure; // True if any of the operations failed
-        }
-
-        @Override
-        protected Boolean doInBackground(Void... params) {
-            return cropBitmap();
-        }
-
-        @Override
-        protected void onPostExecute(Boolean result) {
-            if (mOnEndRunnable != null) {
-                mOnEndRunnable.run();
-            }
-        }
-    }
-
     protected void updateWallpaperDimensions(int width, int height) {
         String spKey = getSharedPreferencesKey();
         SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
@@ -835,11 +348,11 @@
                 sp, getWindowManager(), WallpaperManager.getInstance(this), true);
     }
 
-    static public void suggestWallpaperDimension(Resources res,
+    public static void suggestWallpaperDimension(Resources res,
             final SharedPreferences sharedPrefs,
             WindowManager windowManager,
             final WallpaperManager wallpaperManager, boolean fallBackToDefaults) {
-        final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager);
+        final Point defaultWallpaperSize = BitmapUtils.getDefaultWallpaperSize(res, windowManager);
         // If we have saved a wallpaper width/height, use that instead
 
         int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1);
@@ -859,40 +372,4 @@
             wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
         }
     }
-
-    protected static RectF getMaxCropRect(
-            int inWidth, int inHeight, int outWidth, int outHeight, boolean leftAligned) {
-        RectF cropRect = new RectF();
-        // Get a crop rect that will fit this
-        if (inWidth / (float) inHeight > outWidth / (float) outHeight) {
-             cropRect.top = 0;
-             cropRect.bottom = inHeight;
-             cropRect.left = (inWidth - (outWidth / (float) outHeight) * inHeight) / 2;
-             cropRect.right = inWidth - cropRect.left;
-             if (leftAligned) {
-                 cropRect.right -= cropRect.left;
-                 cropRect.left = 0;
-             }
-        } else {
-            cropRect.left = 0;
-            cropRect.right = inWidth;
-            cropRect.top = (inHeight - (outHeight / (float) outWidth) * inWidth) / 2;
-            cropRect.bottom = inHeight - cropRect.top;
-        }
-        return cropRect;
-    }
-
-    protected static CompressFormat convertExtensionToCompressFormat(String extension) {
-        return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
-    }
-
-    protected 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/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
index 043f630..53cc13d 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
@@ -70,6 +70,9 @@
 import android.widget.LinearLayout;
 import android.widget.Toast;
 
+import com.android.gallery3d.common.BitmapCropTask;
+import com.android.gallery3d.common.BitmapUtils;
+import com.android.gallery3d.common.Utils;
 import com.android.photos.BitmapRegionTileSource;
 import com.android.photos.BitmapRegionTileSource.BitmapSource;
 
@@ -174,7 +177,7 @@
         @Override
         public void onSave(final WallpaperPickerActivity a) {
             boolean finishActivityWhenDone = true;
-            OnBitmapCroppedHandler h = new OnBitmapCroppedHandler() {
+            BitmapCropTask.OnBitmapCroppedHandler h = new BitmapCropTask.OnBitmapCroppedHandler() {
                 public void onBitmapCropped(byte[] imageBytes) {
                     Point thumbSize = getDefaultThumbnailSize(a.getResources());
                     // rotation is set to 0 since imageBytes has already been correctly rotated
@@ -240,9 +243,9 @@
             BitmapRegionTileSource source = new BitmapRegionTileSource(a, bitmapSource);
             CropView v = a.getCropView();
             v.setTileSource(source, null);
-            Point wallpaperSize = WallpaperCropActivity.getDefaultWallpaperSize(
+            Point wallpaperSize = BitmapUtils.getDefaultWallpaperSize(
                     a.getResources(), a.getWindowManager());
-            RectF crop = WallpaperCropActivity.getMaxCropRect(
+            RectF crop = Utils.getMaxCropRect(
                     source.getImageWidth(), source.getImageHeight(),
                     wallpaperSize.x, wallpaperSize.y, false);
             v.setScale(wallpaperSize.x / crop.width());
@@ -811,7 +814,7 @@
         rotatedBounds[0] = Math.abs(rotatedBounds[0]);
         rotatedBounds[1] = Math.abs(rotatedBounds[1]);
 
-        RectF cropRect = WallpaperCropActivity.getMaxCropRect(
+        RectF cropRect = Utils.getMaxCropRect(
                 (int) rotatedBounds[0], (int) rotatedBounds[1], width, height, leftAligned);
         cropTask.setCropBounds(cropRect);
 
@@ -838,7 +841,7 @@
         new AsyncTask<Void, Bitmap, Bitmap>() {
             protected Bitmap doInBackground(Void...args) {
                 try {
-                    int rotation = WallpaperCropActivity.getRotationFromExif(context, uri);
+                    int rotation = BitmapUtils.getRotationFromExif(context, uri);
                     return createThumbnail(defaultSize, context, uri, null, null, 0, rotation, false);
                 } catch (SecurityException securityException) {
                     if (isDestroyed()) {
@@ -1023,7 +1026,7 @@
         } else {
             Resources res = getResources();
             Point defaultThumbSize = getDefaultThumbnailSize(res);
-            int rotation = WallpaperCropActivity.getRotationFromExif(res, resId);
+            int rotation = BitmapUtils.getRotationFromExif(res, resId);
             thumb = createThumbnail(
                     defaultThumbSize, this, null, null, sysRes, resId, rotation, false);
             if (thumb != null) {
diff --git a/WallpaperPicker/src/android/util/Pools.java b/WallpaperPicker/src/com/android/photos/views/Pools.java
similarity index 98%
rename from WallpaperPicker/src/android/util/Pools.java
rename to WallpaperPicker/src/com/android/photos/views/Pools.java
index 40bab1e..c60f2f0 100644
--- a/WallpaperPicker/src/android/util/Pools.java
+++ b/WallpaperPicker/src/com/android/photos/views/Pools.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package android.util;
+package com.android.photos.views;
 
 /**
  * Helper class for crating pools of objects. An example use looks like this:
diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java
index b0292e6..f9b7ab4 100644
--- a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java
+++ b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java
@@ -23,8 +23,6 @@
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.LongSparseArray;
-import android.util.Pools.Pool;
-import android.util.Pools.SynchronizedPool;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -32,6 +30,8 @@
 import com.android.gallery3d.glrenderer.BasicTexture;
 import com.android.gallery3d.glrenderer.GLCanvas;
 import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.photos.views.Pools.Pool;
+import com.android.photos.views.Pools.SynchronizedPool;
 
 /**
  * Handles laying out, decoding, and drawing of tiles in GL