Initial implementation of new wallpaper picker.

Change-Id: Ib4c5ac4989b4959fa62465d9cde3cac662e24949
diff --git a/src/com/android/launcher3/CropView.java b/src/com/android/launcher3/CropView.java
new file mode 100644
index 0000000..5b49282
--- /dev/null
+++ b/src/com/android/launcher3/CropView.java
@@ -0,0 +1,168 @@
+/*
+ * 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.launcher3;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+
+import com.android.photos.views.TiledImageRenderer.TileSource;
+import com.android.photos.views.TiledImageView;
+
+public class CropView extends TiledImageView implements OnScaleGestureListener {
+
+    private ScaleGestureDetector mScaleGestureDetector;
+    private float mLastX, mLastY;
+    private float mMinScale;
+
+    public CropView(Context context) {
+        this(context, null);
+    }
+
+    public CropView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mScaleGestureDetector = new ScaleGestureDetector(context, this);
+    }
+
+    public RectF getCrop() {
+        final float width = getWidth();
+        final float height = getHeight();
+        final float imageWidth = mRenderer.source.getImageWidth();
+        final float imageHeight = mRenderer.source.getImageHeight();
+        final float scale = mRenderer.scale;
+        float centerX = (width / 2f - mRenderer.centerX + (imageWidth - width) / 2f)
+                * scale + width / 2f;
+        float centerY = (height / 2f - mRenderer.centerY + (imageHeight - height) / 2f)
+                * scale + height / 2f;
+        float leftEdge = centerX - imageWidth / 2f * scale;
+        float topEdge = centerY - imageHeight / 2f * scale;
+
+        float cropLeft = -leftEdge / scale;
+        float cropTop = -topEdge / scale;
+        float cropRight = cropLeft + width / scale;
+        float cropBottom = cropTop + height / scale;
+        RectF cropRect = new RectF(cropLeft, cropTop, cropRight, cropBottom);
+
+        return new RectF(cropLeft, cropTop, cropRight, cropBottom);
+    }
+
+    public void setTileSource(TileSource source, Runnable isReadyCallback) {
+        super.setTileSource(source, isReadyCallback);
+        updateMinScale(getWidth(), getHeight(), source);
+    }
+
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        updateMinScale(w, h, mRenderer.source);
+    }
+
+    private void updateMinScale(int w, int h, TileSource source) {
+        synchronized (mLock) {
+            if (source != null) {
+                mMinScale = Math.max(w / (float) source.getImageWidth(),
+                        h / (float) source.getImageHeight());
+                mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
+            }
+        }
+    }
+
+    @Override
+    public boolean onScaleBegin(ScaleGestureDetector detector) {
+        return true;
+    }
+
+    @Override
+    public boolean onScale(ScaleGestureDetector detector) {
+        // Don't need the lock because this will only fire inside of
+        // onTouchEvent
+        mRenderer.scale *= detector.getScaleFactor();
+        mRenderer.scale = Math.max(mMinScale, mRenderer.scale);
+        invalidate();
+        return true;
+    }
+
+    @Override
+    public void onScaleEnd(ScaleGestureDetector detector) {
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        int action = event.getActionMasked();
+        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
+        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
+
+        // Determine focal point
+        float sumX = 0, sumY = 0;
+        final int count = event.getPointerCount();
+        for (int i = 0; i < count; i++) {
+            if (skipIndex == i)
+                continue;
+            sumX += event.getX(i);
+            sumY += event.getY(i);
+        }
+        final int div = pointerUp ? count - 1 : count;
+        float x = sumX / div;
+        float y = sumY / div;
+
+        synchronized (mLock) {
+            mScaleGestureDetector.onTouchEvent(event);
+            switch (action) {
+                case MotionEvent.ACTION_MOVE:
+                    mRenderer.centerX += (mLastX - x) / mRenderer.scale;
+                    mRenderer.centerY += (mLastY - y) / mRenderer.scale;
+                    invalidate();
+                    break;
+            }
+            if (mRenderer.source != null) {
+                // Adjust position so that the wallpaper covers the entire area
+                // of the screen
+                final float width = getWidth();
+                final float height = getHeight();
+                final float imageWidth = mRenderer.source.getImageWidth();
+                final float imageHeight = mRenderer.source.getImageHeight();
+                final float scale = mRenderer.scale;
+                float centerX = (width / 2f - mRenderer.centerX + (imageWidth - width) / 2f)
+                        * scale + width / 2f;
+                float centerY = (height / 2f - mRenderer.centerY + (imageHeight - height) / 2f)
+                        * scale + height / 2f;
+                float leftEdge = centerX - imageWidth / 2f * scale;
+                float rightEdge = centerX + imageWidth / 2f * scale;
+                float topEdge = centerY - imageHeight / 2f * scale;
+                float bottomEdge = centerY + imageHeight / 2f * scale;
+                if (leftEdge > 0) {
+                    mRenderer.centerX += Math.ceil(leftEdge / scale);
+                }
+                if (rightEdge < getWidth()) {
+                    mRenderer.centerX += (rightEdge - getWidth()) / scale;
+                }
+                if (topEdge > 0) {
+                    mRenderer.centerY += Math.ceil(topEdge / scale);
+                }
+                if (bottomEdge < getHeight()) {
+                    mRenderer.centerY += (bottomEdge - getHeight()) / scale;
+                }
+            }
+        }
+
+        mLastX = x;
+        mLastY = y;
+        return true;
+    }
+}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 9ffc572..a16a33e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2040,20 +2040,9 @@
     private void startWallpaper() {
         showWorkspace(true);
         final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER);
-        Intent chooser = Intent.createChooser(pickWallpaper,
-                getText(R.string.chooser_wallpaper));
-        // NOTE: Adds a configure option to the chooser if the wallpaper supports it
-        //       Removed in Eclair MR1
-//        WallpaperManager wm = (WallpaperManager)
-//                getSystemService(Context.WALLPAPER_SERVICE);
-//        WallpaperInfo wi = wm.getWallpaperInfo();
-//        if (wi != null && wi.getSettingsActivity() != null) {
-//            LabeledIntent li = new LabeledIntent(getPackageName(),
-//                    R.string.configure_wallpaper, 0);
-//            li.setClassName(wi.getPackageName(), wi.getSettingsActivity());
-//            chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { li });
-//        }
-        startActivityForResult(chooser, REQUEST_PICK_WALLPAPER);
+        pickWallpaper.setComponent(
+                new ComponentName(getPackageName(), WallpaperPickerActivity.class.getName()));
+        startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER);
     }
 
     /**
@@ -4123,6 +4112,9 @@
             return null;
         }
     }
+    protected SharedPreferences getSharedPrefs() {
+        return mSharedPrefs;
+    }
     public boolean isFolderClingVisible() {
         Cling cling = (Cling) findViewById(R.id.folder_cling);
         if (cling != null) {
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 0c577e5..53d2ec5 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -19,6 +19,7 @@
 import android.app.SearchManager;
 import android.content.*;
 import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.provider.Settings;
@@ -76,7 +77,7 @@
         }
 
         // set sIsScreenXLarge and mScreenDensity *before* creating icon cache
-        mIsScreenLarge = sContext.getResources().getBoolean(R.bool.is_large_tablet);
+        mIsScreenLarge = isScreenLarge(sContext.getResources());
         mScreenDensity = sContext.getResources().getDisplayMetrics().density;
 
         mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(sContext);
@@ -188,6 +189,11 @@
         return mIsScreenLarge;
     }
 
+    // Need a version that doesn't require an instance of LauncherAppState for the wallpaper picker
+    public static boolean isScreenLarge(Resources res) {
+        return res.getBoolean(R.bool.is_large_tablet);
+    }
+
     public static boolean isScreenLandscape(Context context) {
         return context.getResources().getConfiguration().orientation ==
             Configuration.ORIENTATION_LANDSCAPE;
diff --git a/src/com/android/launcher3/PreloadReceiver.java b/src/com/android/launcher3/PreloadReceiver.java
index 4c9032f..75e5c98 100644
--- a/src/com/android/launcher3/PreloadReceiver.java
+++ b/src/com/android/launcher3/PreloadReceiver.java
@@ -31,8 +31,7 @@
 
     @Override
     public void onReceive(Context context, Intent intent) {
-        final LauncherAppState app = LauncherAppState.getInstance();
-        final LauncherProvider provider = app.getLauncherProvider();
+        final LauncherProvider provider = LauncherAppState.getLauncherProvider();
         if (provider != null) {
             String name = intent.getStringExtra(EXTRA_WORKSPACE_NAME);
             final int workspaceResId = !TextUtils.isEmpty(name)
diff --git a/src/com/android/launcher3/WallpaperCropActivity.java b/src/com/android/launcher3/WallpaperCropActivity.java
new file mode 100644
index 0000000..087785e
--- /dev/null
+++ b/src/com/android/launcher3/WallpaperCropActivity.java
@@ -0,0 +1,289 @@
+/*
+ * 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.launcher3;
+
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+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 com.android.gallery3d.common.Utils;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+// LAUNCHER crop activity!
+public class WallpaperCropActivity extends Activity {
+    private static final String LOGTAG = "Launcher3.CropActivity";
+
+    private int mOutputX = 0;
+    private int mOutputY = 0;
+
+    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
+     * have some overhead to hit so that we go way below the limit here to make
+     * sure the intent stays below 1MB.We should consider just returning a byte
+     * array instead of a Bitmap instance to avoid overhead.
+     */
+    public static final int MAX_BMAP_IN_INTENT = 750000;
+
+
+    protected class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {
+        Uri mInUri = null;
+        InputStream mInStream;
+        RectF mCropBounds = null;
+        int mOutWidth, mOutHeight;
+        int mRotation = 0; // for now
+        protected final WallpaperManager mWPManager;
+        String mOutputFormat = "jpg"; // for now
+        boolean mSetWallpaper;
+        boolean mSaveCroppedBitmap;
+        Bitmap mCroppedBitmap;
+
+        public BitmapCropTask(Uri inUri, RectF cropBounds, int outWidth, int outHeight,
+                boolean setWallpaper, boolean saveCroppedBitmap) {
+            mInUri = inUri;
+            mCropBounds = cropBounds;
+            mOutWidth = outWidth;
+            mOutHeight = outHeight;
+            mWPManager = WallpaperManager.getInstance(getApplicationContext());
+            mSetWallpaper = setWallpaper;
+            mSaveCroppedBitmap = saveCroppedBitmap;
+        }
+
+        // Helper to setup input stream
+        private void regenerateInputStream() {
+            if (mInUri == null) {
+                Log.w(LOGTAG, "cannot read original file, no input URI given");
+            } else {
+                Utils.closeSilently(mInStream);
+                try {
+                    mInStream = new BufferedInputStream(
+                            getContentResolver().openInputStream(mInUri));
+                } catch (FileNotFoundException e) {
+                    Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+                }
+            }
+        }
+
+        public Point getImageBounds() {
+            regenerateInputStream();
+            if (mInStream != null) {
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                options.inJustDecodeBounds = true;
+                BitmapFactory.decodeStream(mInStream, null, options);
+                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;
+
+            regenerateInputStream();
+
+            if (mInStream != null) {
+                // Find crop bounds (scaled to original image size)
+                Rect roundedTrueCrop = new Rect();
+                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.min(roundedTrueCrop.width() / mOutWidth,
+                        roundedTrueCrop.height() / mOutHeight);
+
+                // Attempt to open a region decoder
+                BitmapRegionDecoder decoder = null;
+                try {
+                    decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+                } catch (IOException e) {
+                    Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+                }
+
+                Bitmap crop = null;
+                if (decoder != null) {
+                    // Do region decoding to get crop bitmap
+                    BitmapFactory.Options options = new BitmapFactory.Options();
+                    if (scaleDownSampleSize > 1) {
+                        options.inSampleSize = scaleDownSampleSize;
+                    }
+                    crop = decoder.decodeRegion(roundedTrueCrop, options);
+                    decoder.recycle();
+                }
+
+                if (crop == null) {
+                    // BitmapRegionDecoder has failed, try to crop in-memory
+                    regenerateInputStream();
+                    Bitmap fullSize = null;
+                    if (mInStream != null) {
+                        BitmapFactory.Options options = new BitmapFactory.Options();
+                        if (scaleDownSampleSize > 1) {
+                            options.inSampleSize = scaleDownSampleSize;
+                        }
+                        fullSize = BitmapFactory.decodeStream(mInStream, null, options);
+                    }
+                    if (fullSize != null) {
+                        crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+                                roundedTrueCrop.top, roundedTrueCrop.width(),
+                                roundedTrueCrop.height());
+                    }
+                }
+
+                if (crop == null) {
+                    Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
+                    failure = true;
+                    return false;
+                }
+                if (mOutputX > 0 && mOutputY > 0) {
+                    Matrix m = new Matrix();
+                    RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
+                    if (mRotation > 0) {
+                        m.setRotate(mRotation);
+                        m.mapRect(cropRect);
+                    }
+                    RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
+                    m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+                    m.preRotate(mRotation);
+                    Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+                            (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+                    if (tmp != null) {
+                        Canvas c = new Canvas(tmp);
+                        c.drawBitmap(crop, m, new Paint());
+                        crop = tmp;
+                    }
+                } else if (mRotation > 0) {
+                    Matrix m = new Matrix();
+                    m.setRotate(mRotation);
+                    Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
+                            crop.getHeight(), m, true);
+                    if (tmp != null) {
+                        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 && mWPManager != null) {
+                        if (mWPManager == null) {
+                            Log.w(LOGTAG, "no wallpaper manager");
+                            failure = true;
+                        } else {
+                            try {
+                                mWPManager.setStream(new ByteArrayInputStream(tmpOut
+                                        .toByteArray()));
+                                updateWallpaperDimensions(mOutWidth, mOutHeight);
+                            } 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) {
+            setResult(Activity.RESULT_OK);
+            finish();
+        }
+    }
+
+    protected void updateWallpaperDimensions(int width, int height) {
+        String spKey = LauncherAppState.getSharedPreferencesKey();
+        SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = sp.edit();
+        if (width != 0 && height != 0) {
+            editor.putInt(WALLPAPER_WIDTH_KEY, width);
+            editor.putInt(WALLPAPER_HEIGHT_KEY, height);
+        } else {
+            editor.remove(WALLPAPER_WIDTH_KEY);
+            editor.remove(WALLPAPER_HEIGHT_KEY);
+        }
+        editor.commit();
+        WallpaperPickerActivity.suggestWallpaperDimension(getResources(),
+                sp, getWindowManager(), WallpaperManager.getInstance(this));
+    }
+
+    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/src/com/android/launcher3/WallpaperPickerActivity.java b/src/com/android/launcher3/WallpaperPickerActivity.java
new file mode 100644
index 0000000..0327421
--- /dev/null
+++ b/src/com/android/launcher3/WallpaperPickerActivity.java
@@ -0,0 +1,441 @@
+/*
+ * 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.launcher3;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.WallpaperManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LevelListDrawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+
+import com.android.photos.BitmapRegionTileSource;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class WallpaperPickerActivity extends WallpaperCropActivity {
+    private static final String TAG = "Launcher.WallpaperPickerActivity";
+
+    private static final int IMAGE_PICK = 5;
+    private static final float WALLPAPER_SCREENS_SPAN = 2f;
+
+    private ArrayList<Integer> mThumbs;
+    private ArrayList<Integer> mImages;
+
+    private View mSelectedThumb;
+    private CropView mCropView;
+
+    private static class ThumbnailMetaData {
+        public boolean mLaunchesGallery;
+        public Uri mGalleryImageUri;
+        public int mWallpaperResId;
+    }
+
+    private OnClickListener mThumbnailOnClickListener = new OnClickListener() {
+        public void onClick(View v) {
+            if (mSelectedThumb != null) {
+                mSelectedThumb.setSelected(false);
+            }
+
+            ThumbnailMetaData meta = (ThumbnailMetaData) v.getTag();
+
+            if (!meta.mLaunchesGallery) {
+                mSelectedThumb = v;
+                v.setSelected(true);
+            }
+
+            if (meta.mLaunchesGallery) {
+                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+                intent.setType("image/*");
+                startActivityForResult(intent, IMAGE_PICK);
+            } else if (meta.mGalleryImageUri != null) {
+                mCropView.setTileSource(new BitmapRegionTileSource(WallpaperPickerActivity.this,
+                        meta.mGalleryImageUri, 1024, 0), null);
+            } else {
+                mCropView.setTileSource(new BitmapRegionTileSource(WallpaperPickerActivity.this,
+                        meta.mWallpaperResId, 1024, 0), null);
+            }
+        }
+    };
+
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == IMAGE_PICK && resultCode == RESULT_OK) {
+            Uri uri = data.getData();
+
+            // Add a tile for the image picked from Gallery
+            LinearLayout wallpapers = (LinearLayout) findViewById(R.id.wallpaper_list);
+            FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater().
+                    inflate(R.layout.wallpaper_picker_item, wallpapers, false);
+            setWallpaperItemPaddingToZero(pickedImageThumbnail);
+
+            // Load the thumbnail
+            ImageView image = (ImageView) pickedImageThumbnail.findViewById(R.id.wallpaper_image);
+
+            Resources res = getResources();
+            int width = res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth);
+            int height = res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight);
+
+            BitmapCropTask cropTask = new BitmapCropTask(uri, null, width, height, false, true);
+            Point bounds = cropTask.getImageBounds();
+
+            RectF cropRect = new RectF();
+            // Get a crop rect that will fit this
+            if (bounds.x / (float) bounds.y > width / (float) height) {
+                 cropRect.top = 0;
+                 cropRect.bottom = bounds.y;
+                 cropRect.left = (bounds.x - (width / (float) height) * bounds.y) / 2;
+                 cropRect.right = bounds.x - cropRect.left;
+            } else {
+                cropRect.left = 0;
+                cropRect.right = bounds.x;
+                cropRect.top = (bounds.y - (height / (float) width) * bounds.x) / 2;
+                cropRect.bottom = bounds.y - cropRect.top;
+            }
+            cropTask.setCropBounds(cropRect);
+
+            if (cropTask.cropBitmap()) {
+                image.setImageBitmap(cropTask.getCroppedBitmap());
+                Drawable thumbDrawable = image.getDrawable();
+                thumbDrawable.setDither(true);
+            } else {
+                Log.e(TAG, "Error loading thumbnail for uri=" + uri);
+            }
+            wallpapers.addView(pickedImageThumbnail, 0);
+
+            ThumbnailMetaData meta = new ThumbnailMetaData();
+            meta.mGalleryImageUri = uri;
+            pickedImageThumbnail.setTag(meta);
+            mThumbnailOnClickListener.onClick(pickedImageThumbnail);
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.wallpaper_picker);
+
+        mCropView = (CropView) findViewById(R.id.cropView);
+
+        // Populate the built-in wallpapers
+        findWallpapers();
+
+        LinearLayout wallpapers = (LinearLayout) findViewById(R.id.wallpaper_list);
+        ImageAdapter ia = new ImageAdapter(this);
+        for (int i = 0; i < ia.getCount(); i++) {
+            FrameLayout thumbnail = (FrameLayout) ia.getView(i, null, wallpapers);
+            wallpapers.addView(thumbnail, i);
+
+            ThumbnailMetaData meta = new ThumbnailMetaData();
+            meta.mWallpaperResId = mImages.get(i);
+            thumbnail.setTag(meta);
+            thumbnail.setOnClickListener(mThumbnailOnClickListener);
+            if (i == 0) {
+                mThumbnailOnClickListener.onClick(thumbnail);
+            }
+        }
+        // Add a tile for the Gallery
+        FrameLayout galleryThumbnail = (FrameLayout) getLayoutInflater().
+                inflate(R.layout.wallpaper_picker_gallery_item, wallpapers, false);
+        setWallpaperItemPaddingToZero(galleryThumbnail);
+
+        TextView galleryLabel =
+                (TextView) galleryThumbnail.findViewById(R.id.wallpaper_item_label);
+        galleryLabel.setText(R.string.gallery);
+        wallpapers.addView(galleryThumbnail, 0);
+
+        ThumbnailMetaData meta = new ThumbnailMetaData();
+        meta.mLaunchesGallery = true;
+        galleryThumbnail.setTag(meta);
+        galleryThumbnail.setOnClickListener(mThumbnailOnClickListener);
+
+        // Action bar
+        // Show the custom action bar view
+        final ActionBar actionBar = getActionBar();
+        actionBar.setCustomView(R.layout.actionbar_set_wallpaper);
+        actionBar.getCustomView().setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+
+                        ThumbnailMetaData meta = (ThumbnailMetaData) mSelectedThumb.getTag();
+                        if (meta.mLaunchesGallery) {
+                            // shouldn't be selected, but do nothing
+                        } else if (meta.mGalleryImageUri != null) {
+                            // Get the crop
+                            // TODO: get outwidth/outheight more robustly?
+                            BitmapCropTask cropTask = new BitmapCropTask(meta.mGalleryImageUri,
+                                    mCropView.getCrop(), mCropView.getWidth(), mCropView.getHeight(),
+                                    true, false);
+
+                            cropTask.execute();
+                        } else if (meta.mWallpaperResId != 0) {
+                            try {
+                                WallpaperManager wm =
+                                        WallpaperManager.getInstance(getApplicationContext());
+                                wm.setResource(meta.mWallpaperResId);
+                                // passing 0 will just revert back to using the default wallpaper
+                                // size (setWallpaperDimension)
+                                updateWallpaperDimensions(0, 0);
+                                String spKey = LauncherAppState.getSharedPreferencesKey();
+                                SharedPreferences sp =
+                                        getSharedPreferences(spKey, Context.MODE_PRIVATE);
+                                SharedPreferences.Editor editor = sp.edit();
+                                editor.remove(WALLPAPER_WIDTH_KEY);
+                                editor.remove(WALLPAPER_HEIGHT_KEY);
+                                editor.commit();
+                                setResult(Activity.RESULT_OK);
+                                finish();
+                            } catch (IOException e) {
+                                Log.e(TAG, "Failed to set wallpaper: " + e);
+                            }
+                        }
+                    }
+                });
+    }
+
+    private static void setWallpaperItemPaddingToZero(FrameLayout frameLayout) {
+        frameLayout.setPadding(0, 0, 0, 0);
+        frameLayout.setForeground(new ZeroPaddingDrawable(frameLayout.getForeground()));
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        final Intent pickWallpaperIntent = new Intent(Intent.ACTION_SET_WALLPAPER);
+        final PackageManager pm = getPackageManager();
+        final List<ResolveInfo> apps =
+                pm.queryIntentActivities(pickWallpaperIntent, 0);
+
+        SubMenu sub = menu.addSubMenu("Other\u2026"); // TODO: what's the better way to do this?
+        sub.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+
+
+        // Get list of image picker intents
+        Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT);
+        pickImageIntent.setType("image/*");
+        final List<ResolveInfo> imagePickerActivities =
+                pm.queryIntentActivities(pickImageIntent, 0);
+        final ComponentName[] imageActivities = new ComponentName[imagePickerActivities.size()];
+        for (int i = 0; i < imagePickerActivities.size(); i++) {
+            ActivityInfo activityInfo = imagePickerActivities.get(i).activityInfo;
+            imageActivities[i] = new ComponentName(activityInfo.packageName, activityInfo.name);
+        }
+
+        outerLoop:
+        for (ResolveInfo info : apps) {
+            final ComponentName componentName =
+                    new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
+            // Exclude anything from our own package, and the old Launcher
+            if (componentName.getPackageName().equals(getPackageName()) ||
+                    componentName.getPackageName().equals("com.android.launcher")) {
+                continue;
+            }
+            // Exclude any package that already responds to the image picker intent
+            for (ResolveInfo imagePickerActivityInfo : imagePickerActivities) {
+                if (componentName.getPackageName().equals(
+                        imagePickerActivityInfo.activityInfo.packageName)) {
+                    continue outerLoop;
+                }
+            }
+            MenuItem mi = sub.add(info.loadLabel(pm));
+            Drawable icon = info.loadIcon(pm);
+            if (icon != null) {
+                mi.setIcon(icon);
+            }
+        }
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    private void findWallpapers() {
+        mThumbs = new ArrayList<Integer>(24);
+        mImages = new ArrayList<Integer>(24);
+
+        final Resources resources = getResources();
+        // Context.getPackageName() may return the "original" package name,
+        // com.android.launcher3; Resources needs the real package name,
+        // com.android.launcher3. So we ask Resources for what it thinks the
+        // package name should be.
+        final String packageName = resources.getResourcePackageName(R.array.wallpapers);
+
+        addWallpapers(resources, packageName, R.array.wallpapers);
+        addWallpapers(resources, packageName, R.array.extra_wallpapers);
+    }
+
+    private void addWallpapers(Resources resources, String packageName, int list) {
+        final String[] extras = resources.getStringArray(list);
+        for (String extra : extras) {
+            int res = resources.getIdentifier(extra, "drawable", packageName);
+            if (res != 0) {
+                final int thumbRes = resources.getIdentifier(extra + "_small",
+                        "drawable", packageName);
+
+                if (thumbRes != 0) {
+                    mThumbs.add(thumbRes);
+                    mImages.add(res);
+                    // Log.d(TAG, "add: [" + packageName + "]: " + extra + " (" + res + ")");
+                }
+            }
+        }
+    }
+
+    // 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 public void suggestWallpaperDimension(Resources res,
+            final SharedPreferences sharedPrefs,
+            WindowManager windowManager,
+            final WallpaperManager wallpaperManager) {
+        Point minDims = new Point();
+        Point maxDims = new Point();
+        windowManager.getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
+
+        final int maxDim = Math.max(maxDims.x, maxDims.y);
+        final int minDim = Math.min(minDims.x, minDims.y);
+
+        // We need to ensure that there is enough extra space in the wallpaper
+        // for the intended
+        // parallax effects
+        final int defaultWidth, defaultHeight;
+        if (LauncherAppState.isScreenLarge(res)) {
+            defaultWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
+            defaultHeight = maxDim;
+        } else {
+            defaultWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
+            defaultHeight = maxDim;
+        }
+        new Thread("suggestWallpaperDimension") {
+            public void run() {
+                // If we have saved a wallpaper width/height, use that instead
+                int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, defaultWidth);
+                int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, defaultHeight);
+            }
+        }.start();
+    }
+
+    static class ZeroPaddingDrawable extends LevelListDrawable {
+        public ZeroPaddingDrawable(Drawable d) {
+            super();
+            addLevel(0, 0, d);
+            setLevel(0);
+        }
+
+        @Override
+        public boolean getPadding(Rect padding) {
+            padding.set(0, 0, 0, 0);
+            return true;
+        }
+    }
+
+    private class ImageAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter {
+        private LayoutInflater mLayoutInflater;
+
+        ImageAdapter(Activity activity) {
+            mLayoutInflater = activity.getLayoutInflater();
+        }
+
+        public int getCount() {
+            return mThumbs.size();
+        }
+
+        public Object getItem(int position) {
+            return position;
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view;
+
+            if (convertView == null) {
+                view = mLayoutInflater.inflate(R.layout.wallpaper_picker_item, parent, false);
+            } else {
+                view = convertView;
+            }
+
+            setWallpaperItemPaddingToZero((FrameLayout) view);
+
+            ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image);
+
+            int thumbRes = mThumbs.get(position);
+            image.setImageResource(thumbRes);
+            Drawable thumbDrawable = image.getDrawable();
+            if (thumbDrawable != null) {
+                thumbDrawable.setDither(true);
+            } else {
+                Log.e(TAG, "Error decoding thumbnail resId=" + thumbRes + " for wallpaper #"
+                        + position);
+            }
+
+            return view;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index b2f7433..2298c53 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -104,7 +104,6 @@
     private LayoutTransition mLayoutTransition;
     private final WallpaperManager mWallpaperManager;
     private IBinder mWindowToken;
-    private static final float WALLPAPER_SCREENS_SPAN = 2f;
 
     private int mDefaultPage;
 
@@ -189,16 +188,11 @@
     public static final int DRAG_BITMAP_PADDING = 2;
     private boolean mWorkspaceFadeInAdjacentScreens;
 
-    enum WallpaperVerticalOffset { TOP, MIDDLE, BOTTOM };
-    int mWallpaperWidth;
-    int mWallpaperHeight;
     WallpaperOffsetInterpolator mWallpaperOffset;
     boolean mUpdateWallpaperOffsetImmediately = false;
     private Runnable mDelayedResizeRunnable;
     private Runnable mDelayedSnapToPageRunnable;
     private Point mDisplaySize = new Point();
-    private boolean mIsStaticWallpaper;
-    private int mWallpaperTravelWidth;
     private int mCameraDistance;
 
     // Variables relating to the creation of user folders by hovering shortcuts over shortcuts
@@ -397,8 +391,6 @@
         mWallpaperOffset = new WallpaperOffsetInterpolator();
         Display display = mLauncher.getWindowManager().getDefaultDisplay();
         display.getSize(mDisplaySize);
-        mWallpaperTravelWidth = (int) (mDisplaySize.x *
-                wallpaperTravelToScreenWidthRatio(mDisplaySize.x, mDisplaySize.y));
 
         mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx);
         mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity);
@@ -529,7 +521,7 @@
         mWorkspaceScreens.remove(EXTRA_EMPTY_SCREEN_ID);
         mScreenOrder.remove(EXTRA_EMPTY_SCREEN_ID);
 
-        long newId = LauncherAppState.getInstance().getLauncherProvider().generateNewScreenId();
+        long newId = LauncherAppState.getLauncherProvider().generateNewScreenId();
         mWorkspaceScreens.put(newId, cl);
         mScreenOrder.add(newId);
 
@@ -874,7 +866,6 @@
         // Only show page outlines as we pan if we are on large screen
         if (LauncherAppState.getInstance().isScreenLarge()) {
             showOutlines();
-            mIsStaticWallpaper = mWallpaperManager.getWallpaperInfo() == null;
         }
 
         // If we are not fading in adjacent screens, we still need to restore the alpha in case the
@@ -944,55 +935,9 @@
         Launcher.setScreen(mCurrentPage);
     };
 
-    // As a ratio of screen height, the total distance we want the parallax effect to span
-    // horizontally
-    private 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;
-    }
-
     protected void setWallpaperDimension() {
-        Point minDims = new Point();
-        Point maxDims = new Point();
-        mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
-
-        final int maxDim = Math.max(maxDims.x, maxDims.y);
-        final int minDim = Math.min(minDims.x, minDims.y);
-
-        // We need to ensure that there is enough extra space in the wallpaper for the intended
-        // parallax effects
-        if (LauncherAppState.getInstance().isScreenLarge()) {
-            mWallpaperWidth = (int) (maxDim * wallpaperTravelToScreenWidthRatio(maxDim, minDim));
-            mWallpaperHeight = maxDim;
-        } else {
-            mWallpaperWidth = Math.max((int) (minDim * WALLPAPER_SCREENS_SPAN), maxDim);
-            mWallpaperHeight = maxDim;
-        }
-        new Thread("setWallpaperDimension") {
-            public void run() {
-                mWallpaperManager.suggestDesiredDimensions(mWallpaperWidth, mWallpaperHeight);
-            }
-        }.start();
+        WallpaperPickerActivity.suggestWallpaperDimension(mLauncher.getResources(),
+                mLauncher.getSharedPrefs(), mLauncher.getWindowManager(), mWallpaperManager);
     }
 
     private void syncWallpaperOffsetWithScroll() {