am 3af56b60: (-s ours) Remove duplicate calls to getSortedWidgetsAndShortcuts b/19904873

* commit '3af56b605217e4ff1c6ed79a70fa49eacaf5b02f':
  Remove duplicate calls to getSortedWidgetsAndShortcuts b/19904873
diff --git a/.gitignore b/.gitignore
index 4d5667e..aea5d61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
 *.iml
 .project
 .classpath
+.project.properties
 gen/
 tests/stress/gen/
 WallpaperPicker/gen/
+WallpaperPicker/.project.properties
+bin/
diff --git a/Android.mk b/Android.mk
index 340caca..d7bfdd4 100644
--- a/Android.mk
+++ b/Android.mk
@@ -23,7 +23,7 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v13
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src) \
     $(call all-java-files-under, WallpaperPicker/src) \
@@ -37,6 +37,9 @@
 LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/
 
 LOCAL_SDK_VERSION := 21
+LOCAL_STATIC_JAVA_LIBRARIES := \
+        android-support-v4 \
+        android-support-v7-recyclerview
 
 LOCAL_PACKAGE_NAME := Launcher3
 #LOCAL_CERTIFICATE := shared
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fb7ac3f..b61b90c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -29,12 +29,6 @@
         android:label="@string/permlab_install_shortcut"
         android:description="@string/permdesc_install_shortcut" />
     <permission
-        android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"
-        android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
-        android:protectionLevel="dangerous"
-        android:label="@string/permlab_uninstall_shortcut"
-        android:description="@string/permdesc_uninstall_shortcut"/>
-    <permission
         android:name="com.android.launcher3.permission.READ_SETTINGS"
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
         android:protectionLevel="normal"
@@ -128,7 +122,7 @@
         </activity>
 
         <activity
-            android:name="com.android.launcher3.LauncherWallpaperPickerActivity"
+            android:name="com.android.launcher3.WallpaperPickerActivity"
             android:theme="@style/Theme.WallpaperPicker"
             android:label="@string/pick_wallpaper"
             android:icon="@mipmap/ic_launcher_wallpaper"
@@ -191,15 +185,6 @@
             </intent-filter>
         </receiver>
 
-        <!-- Intent received used to uninstall shortcuts from other applications -->
-        <receiver
-            android:name="com.android.launcher3.UninstallShortcutReceiver"
-            android:permission="com.android.launcher.permission.UNINSTALL_SHORTCUT">
-            <intent-filter>
-                <action android:name="com.android.launcher.action.UNINSTALL_SHORTCUT" />
-            </intent-filter>
-        </receiver>
-
         <!-- Intent received used to initialize a restored widget -->
         <receiver android:name="com.android.launcher3.AppWidgetsRestoredReceiver" >
             <intent-filter>
@@ -207,24 +192,6 @@
             </intent-filter>
         </receiver>
 
-        <!-- New user initialization; set up initial wallpaper -->
-        <receiver
-            android:name="com.android.launcher3.UserInitializeReceiver"
-            android:exported="false">
-            <intent-filter>
-                <action android:name="android.intent.action.USER_INITIALIZE" />
-            </intent-filter>
-        </receiver>
-
-        <receiver android:name="com.android.launcher3.PackageChangedReceiver" >
-            <intent-filter>
-                <action android:name="android.intent.action.PACKAGE_CHANGED"/>
-                <action android:name="android.intent.action.PACKAGE_REPLACED"/>
-                <action android:name="android.intent.action.PACKAGE_REMOVED"/>
-                <data android:scheme="package"></data>
-            </intent-filter>
-        </receiver>
-
         <receiver android:name="com.android.launcher3.StartupReceiver" >
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
diff --git a/WallpaperPicker/AndroidManifest.xml b/WallpaperPicker/AndroidManifest.xml
index 5b6a007..cb1457b 100644
--- a/WallpaperPicker/AndroidManifest.xml
+++ b/WallpaperPicker/AndroidManifest.xml
@@ -4,7 +4,7 @@
         android:versionCode="1"
         android:versionName="1.0"
         >
-  
-    <uses-sdk android:minSdkVersion="15" android:targetSdkVersion="19" />
+
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="21" />
     <application/>
 </manifest>
diff --git a/WallpaperPicker/res/layout/wallpaper_cropper.xml b/WallpaperPicker/res/layout/wallpaper_cropper.xml
index abb8608..ffe8df0 100644
--- a/WallpaperPicker/res/layout/wallpaper_cropper.xml
+++ b/WallpaperPicker/res/layout/wallpaper_cropper.xml
@@ -19,7 +19,6 @@
 -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/wallpaper_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
     <com.android.launcher3.CropView
@@ -28,7 +27,7 @@
         android:layout_height="match_parent" />
     <ProgressBar
         android:id="@+id/loading"
-        style="@android:style/Widget.Holo.ProgressBar.Large"
+        style="?android:attr/progressBarStyleLarge"
         android:visibility="invisible"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/WallpaperPicker/res/layout/wallpaper_picker.xml b/WallpaperPicker/res/layout/wallpaper_picker.xml
index c36493d..0b970b0 100644
--- a/WallpaperPicker/res/layout/wallpaper_picker.xml
+++ b/WallpaperPicker/res/layout/wallpaper_picker.xml
@@ -18,60 +18,74 @@
 */
 -->
 
-<com.android.launcher3.WallpaperRootView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/wallpaper_root"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent" >
+
     <com.android.launcher3.CropView
         android:id="@+id/cropView"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
+
     <ProgressBar
         android:id="@+id/loading"
-        style="@android:style/Widget.Holo.ProgressBar.Large"
-        android:visibility="invisible"
+        style="?android:attr/progressBarStyleLarge"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_centerInParent="true"
+        android:layout_gravity="center"
         android:indeterminate="true"
         android:indeterminateOnly="true"
-        android:background="@android:color/transparent" />
+        android:visibility="invisible" />
+
     <LinearLayout
         android:id="@+id/wallpaper_strip"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_alignParentBottom="true"
+        android:layout_gravity="bottom"
+        android:fitsSystemWindows="true"
         android:orientation="vertical" >
+
         <View
             android:layout_width="match_parent"
             android:layout_height="2dp"
             android:background="@drawable/tile_shadow_top" />
+
         <HorizontalScrollView
             android:id="@+id/wallpaper_scroll_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content" >
-            <LinearLayout android:id="@+id/master_wallpaper_list"
+
+            <LinearLayout
+                android:id="@+id/master_wallpaper_list"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:orientation="horizontal" >
-                <LinearLayout android:id="@+id/wallpaper_list"
+
+                <LinearLayout
+                    android:id="@+id/wallpaper_list"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:orientation="horizontal" />
-                <LinearLayout android:id="@+id/live_wallpaper_list"
+
+                <LinearLayout
+                    android:id="@+id/live_wallpaper_list"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:orientation="horizontal" />
-                <LinearLayout android:id="@+id/third_party_wallpaper_list"
+
+                <LinearLayout
+                    android:id="@+id/third_party_wallpaper_list"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:orientation="horizontal" />
             </LinearLayout>
         </HorizontalScrollView>
+
         <View
             android:layout_width="match_parent"
             android:layout_height="2dp"
             android:background="@drawable/tile_shadow_bottom" />
     </LinearLayout>
-</com.android.launcher3.WallpaperRootView>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml
index ae3c43d..dc65244 100644
--- a/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml
+++ b/WallpaperPicker/res/layout/wallpaper_picker_image_picker_item.xml
@@ -20,7 +20,6 @@
     android:layout_height="@dimen/wallpaperThumbnailHeight"
     android:focusable="true"
     android:clickable="true"
-    android:background="@drawable/wallpaper_tile_fg"
     android:foreground="@drawable/wallpaper_tile_fg">
     <ImageView
         android:id="@+id/wallpaper_image"
diff --git a/WallpaperPicker/res/layout/wallpaper_picker_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_item.xml
index 0ac8f97..3f57fcd 100644
--- a/WallpaperPicker/res/layout/wallpaper_picker_item.xml
+++ b/WallpaperPicker/res/layout/wallpaper_picker_item.xml
@@ -20,7 +20,6 @@
     android:layout_height="@dimen/wallpaperThumbnailHeight"
     android:focusable="true"
     android:clickable="true"
-    android:background="@drawable/wallpaper_tile_fg"
     android:foreground="@drawable/wallpaper_tile_fg">
     <ImageView
         android:id="@+id/wallpaper_image"
diff --git a/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml
index 29fdb1b..2b152fc 100644
--- a/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml
+++ b/WallpaperPicker/res/layout/wallpaper_picker_live_wallpaper_item.xml
@@ -20,7 +20,6 @@
     android:layout_height="@dimen/wallpaperThumbnailHeight"
     android:focusable="true"
     android:clickable="true"
-    android:background="@drawable/wallpaper_tile_fg"
     android:foreground="@drawable/wallpaper_tile_fg">
     <ImageView
         android:id="@+id/wallpaper_image"
diff --git a/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml b/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml
index 68661bc..a7e3a0c 100644
--- a/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml
+++ b/WallpaperPicker/res/layout/wallpaper_picker_third_party_item.xml
@@ -20,7 +20,6 @@
     android:layout_height="@dimen/wallpaperThumbnailHeight"
     android:focusable="true"
     android:clickable="true"
-    android:background="@drawable/wallpaper_tile_fg"
     android:foreground="@drawable/wallpaper_tile_fg">
     <ImageView
         android:id="@+id/wallpaper_image"
diff --git a/WallpaperPicker/res/values-sw720dp-v19/styles.xml b/WallpaperPicker/res/values-sw720dp-v19/styles.xml
index 9107851..d8dab22 100644
--- a/WallpaperPicker/res/values-sw720dp-v19/styles.xml
+++ b/WallpaperPicker/res/values-sw720dp-v19/styles.xml
@@ -18,7 +18,7 @@
 -->
 
 <resources>
-    <style name="Theme" parent="android:Theme.Holo.Wallpaper.NoTitleBar">
+    <style name="Theme" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar">
         <item name="android:windowActionModeOverlay">true</item>
         <item name="android:windowTranslucentStatus">true</item>
         <item name="android:windowTranslucentNavigation">true</item>
diff --git a/WallpaperPicker/res/values-sw720dp/styles.xml b/WallpaperPicker/res/values-sw720dp/styles.xml
index b489090..12f8884 100644
--- a/WallpaperPicker/res/values-sw720dp/styles.xml
+++ b/WallpaperPicker/res/values-sw720dp/styles.xml
@@ -18,7 +18,7 @@
 -->
 
 <resources>
-    <style name="Theme" parent="android:Theme.Holo.Wallpaper.NoTitleBar">
+    <style name="Theme" parent="@android:style/Theme.DeviceDefault.Wallpaper.NoTitleBar">
         <item name="android:windowActionModeOverlay">true</item>
     </style>
 </resources>
diff --git a/WallpaperPicker/src/android/util/Pools.java b/WallpaperPicker/src/android/util/Pools.java
deleted file mode 100644
index 40bab1e..0000000
--- a/WallpaperPicker/src/android/util/Pools.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (C) 2009 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 android.util;
-
-/**
- * Helper class for crating pools of objects. An example use looks like this:
- * <pre>
- * public class MyPooledClass {
- *
- *     private static final SynchronizedPool<MyPooledClass> sPool =
- *             new SynchronizedPool<MyPooledClass>(10);
- *
- *     public static MyPooledClass obtain() {
- *         MyPooledClass instance = sPool.acquire();
- *         return (instance != null) ? instance : new MyPooledClass();
- *     }
- *
- *     public void recycle() {
- *          // Clear state if needed.
- *          sPool.release(this);
- *     }
- *
- *     . . .
- * }
- * </pre>
- *
- * @hide
- */
-public final class Pools {
-
-    /**
-     * Interface for managing a pool of objects.
-     *
-     * @param <T> The pooled type.
-     */
-    public static interface Pool<T> {
-
-        /**
-         * @return An instance from the pool if such, null otherwise.
-         */
-        public T acquire();
-
-        /**
-         * Release an instance to the pool.
-         *
-         * @param instance The instance to release.
-         * @return Whether the instance was put in the pool.
-         *
-         * @throws IllegalStateException If the instance is already in the pool.
-         */
-        public boolean release(T instance);
-    }
-
-    private Pools() {
-        /* do nothing - hiding constructor */
-    }
-
-    /**
-     * Simple (non-synchronized) pool of objects.
-     *
-     * @param <T> The pooled type.
-     */
-    public static class SimplePool<T> implements Pool<T> {
-        private final Object[] mPool;
-
-        private int mPoolSize;
-
-        /**
-         * Creates a new instance.
-         *
-         * @param maxPoolSize The max pool size.
-         *
-         * @throws IllegalArgumentException If the max pool size is less than zero.
-         */
-        public SimplePool(int maxPoolSize) {
-            if (maxPoolSize <= 0) {
-                throw new IllegalArgumentException("The max pool size must be > 0");
-            }
-            mPool = new Object[maxPoolSize];
-        }
-
-        @Override
-        @SuppressWarnings("unchecked")
-        public T acquire() {
-            if (mPoolSize > 0) {
-                final int lastPooledIndex = mPoolSize - 1;
-                T instance = (T) mPool[lastPooledIndex];
-                mPool[lastPooledIndex] = null;
-                mPoolSize--;
-                return instance;
-            }
-            return null;
-        }
-
-        @Override
-        public boolean release(T instance) {
-            if (isInPool(instance)) {
-                throw new IllegalStateException("Already in the pool!");
-            }
-            if (mPoolSize < mPool.length) {
-                mPool[mPoolSize] = instance;
-                mPoolSize++;
-                return true;
-            }
-            return false;
-        }
-
-        private boolean isInPool(T instance) {
-            for (int i = 0; i < mPoolSize; i++) {
-                if (mPool[i] == instance) {
-                    return true;
-                }
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Synchronized) pool of objects.
-     *
-     * @param <T> The pooled type.
-     */
-    public static class SynchronizedPool<T> extends SimplePool<T> {
-        private final Object mLock = new Object();
-
-        /**
-         * Creates a new instance.
-         *
-         * @param maxPoolSize The max pool size.
-         *
-         * @throws IllegalArgumentException If the max pool size is less than zero.
-         */
-        public SynchronizedPool(int maxPoolSize) {
-            super(maxPoolSize);
-        }
-
-        @Override
-        public T acquire() {
-            synchronized (mLock) {
-                return super.acquire();
-            }
-        }
-
-        @Override
-        public boolean release(T element) {
-            synchronized (mLock) {
-                return super.release(element);
-            }
-        }
-    }
-}
\ No newline at end of file
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 a671ed2..9ac5c1b 100644
--- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java
+++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapUtils.java
@@ -16,87 +16,24 @@
 
 package com.android.gallery3d.common;
 
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.os.Build;
-import android.util.FloatMath;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
 import android.util.Log;
 
-import java.io.ByteArrayOutputStream;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
+import com.android.gallery3d.exif.ExifInterface;
+
+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
@@ -104,157 +41,41 @@
                 : 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 int getRotationFromExif(Context context, Uri uri) {
+        return BitmapUtils.getRotationFromExifHelper(null, 0, context, uri);
     }
 
-    public static Bitmap resizeBitmapByScale(
-            Bitmap bitmap, float scale, boolean recycle) {
-        int width = Math.round(bitmap.getWidth() * scale);
-        int height = Math.round(bitmap.getHeight() * scale);
-        if (width == bitmap.getWidth()
-                && height == bitmap.getHeight()) return bitmap;
-        Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
-        Canvas canvas = new Canvas(target);
-        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;
+    public static int getRotationFromExif(Resources res, int resId) {
+        return BitmapUtils.getRotationFromExifHelper(res, resId, null, null);
     }
 
-    private static Bitmap.Config getConfig(Bitmap bitmap) {
-        Bitmap.Config config = bitmap.getConfig();
-        if (config == null) {
-            config = Bitmap.Config.ARGB_8888;
-        }
-        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);
-    }
-
-    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;
-
-        // 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);
-
-        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;
-    }
-
-    public static void recycleSilently(Bitmap bitmap) {
-        if (bitmap == null) return;
+    private static int getRotationFromExifHelper(Resources res, int resId, Context context, Uri uri) {
+        ExifInterface ei = new ExifInterface();
+        InputStream is = null;
+        BufferedInputStream bis = null;
         try {
-            bitmap.recycle();
-        } catch (Throwable t) {
-            Log.w(TAG, "unable recycle bitmap", t);
-        }
-    }
-
-    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 Bitmap createVideoThumbnail(String filePath) {
-        // MediaMetadataRetriever is available on API Level 8
-        // but is hidden until API Level 10
-        Class<?> clazz = null;
-        Object instance = null;
-        try {
-            clazz = Class.forName("android.media.MediaMetadataRetriever");
-            instance = clazz.newInstance();
-
-            Method method = clazz.getMethod("setDataSource", String.class);
-            method.invoke(instance, filePath);
-
-            // The method name changes between API Level 9 and 10.
-            if (Build.VERSION.SDK_INT <= 9) {
-                return (Bitmap) clazz.getMethod("captureFrame").invoke(instance);
+            if (uri != null) {
+                is = context.getContentResolver().openInputStream(uri);
+                bis = new BufferedInputStream(is);
+                ei.readExif(bis);
             } else {
-                byte[] data = (byte[]) clazz.getMethod("getEmbeddedPicture").invoke(instance);
-                if (data != null) {
-                    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
-                    if (bitmap != null) return bitmap;
-                }
-                return (Bitmap) clazz.getMethod("getFrameAtTime").invoke(instance);
+                is = res.openRawResource(resId);
+                bis = new BufferedInputStream(is);
+                ei.readExif(bis);
             }
-        } catch (IllegalArgumentException ex) {
-            // Assume this is a corrupt video file
-        } catch (RuntimeException ex) {
-            // Assume this is a corrupt video file.
-        } catch (InstantiationException e) {
-            Log.e(TAG, "createVideoThumbnail", e);
-        } catch (InvocationTargetException e) {
-            Log.e(TAG, "createVideoThumbnail", e);
-        } catch (ClassNotFoundException e) {
-            Log.e(TAG, "createVideoThumbnail", e);
-        } catch (NoSuchMethodException e) {
-            Log.e(TAG, "createVideoThumbnail", e);
-        } catch (IllegalAccessException e) {
-            Log.e(TAG, "createVideoThumbnail", e);
+            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 {
-            try {
-                if (instance != null) {
-                    clazz.getMethod("release").invoke(instance);
-                }
-            } catch (Exception ignored) {
-            }
+            Utils.closeSilently(bis);
+            Utils.closeSilently(is);
         }
-        return null;
-    }
-
-    public static byte[] compressToBytes(Bitmap bitmap) {
-        return compressToBytes(bitmap, DEFAULT_JPEG_QUALITY);
-    }
-
-    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");
+        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..933260b 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;
@@ -698,6 +696,7 @@
     }
 
     private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) {
+        deleteRecycledResources();
         GLES20.glUseProgram(program);
         checkError();
         enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA);
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/gallery3d/glrenderer/UploadedTexture.java b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java
index f41a979..67abf65 100644
--- a/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java
+++ b/WallpaperPicker/src/com/android/gallery3d/glrenderer/UploadedTexture.java
@@ -20,6 +20,8 @@
 import android.graphics.Bitmap.Config;
 import android.opengl.GLUtils;
 
+import com.android.launcher3.util.Thunk;
+
 import junit.framework.Assert;
 
 import java.util.HashMap;
@@ -82,7 +84,7 @@
         return mIsUploading;
     }
 
-    private static class BorderKey implements Cloneable {
+    @Thunk static class BorderKey implements Cloneable {
         public boolean vertical;
         public Config config;
         public int length;
diff --git a/WallpaperPicker/src/com/android/launcher3/CropView.java b/WallpaperPicker/src/com/android/launcher3/CropView.java
index 578b8ea..50f779a 100644
--- a/WallpaperPicker/src/com/android/launcher3/CropView.java
+++ b/WallpaperPicker/src/com/android/launcher3/CropView.java
@@ -21,7 +21,6 @@
 import android.graphics.Point;
 import android.graphics.RectF;
 import android.util.AttributeSet;
-import android.util.FloatMath;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.ScaleGestureDetector.OnScaleGestureListener;
@@ -300,12 +299,12 @@
                     adjustment[0] = (edges.right - getWidth()) / scale;
                 }
                 if (edges.top > 0) {
-                    adjustment[1] = FloatMath.ceil(edges.top / scale);
+                    adjustment[1] = (float) Math.ceil(edges.top / scale);
                 } else if (edges.bottom < getHeight()) {
                     adjustment[1] = (edges.bottom - getHeight()) / scale;
                 }
                 for (int dim = 0; dim <= 1; dim++) {
-                    if (coef[dim] > 0) adjustment[dim] = FloatMath.ceil(adjustment[dim]);
+                    if (coef[dim] > 0) adjustment[dim] = (float) Math.ceil(adjustment[dim]);
                 }
 
                 mInverseRotateMatrix.mapPoints(adjustment);
diff --git a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java
similarity index 69%
rename from src/com/android/launcher3/LauncherWallpaperPickerActivity.java
rename to WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java
index 10fe013..091c054 100644
--- a/src/com/android/launcher3/LauncherWallpaperPickerActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/LauncherWallpaperPickerActivity.java
@@ -16,15 +16,6 @@
 
 package com.android.launcher3;
 
-import android.content.Intent;
-
+// TODO: Remove this class
 public class LauncherWallpaperPickerActivity extends WallpaperPickerActivity {
-    @Override
-    public void startActivityForResultSafely(Intent intent, int requestCode) {
-        Utilities.startActivityForResultSafely(this, intent, requestCode);
-    }
-    @Override
-    public boolean enableRotation() {
-        return Utilities.isRotationEnabled(this);
-    }
-}
+}
\ No newline at end of file
diff --git a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java
index 88f4461..b53fce1 100644
--- a/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java
+++ b/WallpaperPicker/src/com/android/launcher3/LiveWallpaperListAdapter.java
@@ -30,11 +30,12 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
-import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.ListAdapter;
 import android.widget.TextView;
 
+import com.android.launcher3.util.Thunk;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
@@ -50,7 +51,7 @@
     private final LayoutInflater mInflater;
     private final PackageManager mPackageManager;
 
-    private List<LiveWallpaperTile> mWallpapers;
+    @Thunk List<LiveWallpaperTile> mWallpapers;
 
     @SuppressWarnings("unchecked")
     public LiveWallpaperListAdapter(Context context) {
@@ -90,8 +91,6 @@
             view = convertView;
         }
 
-        WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view);
-
         LiveWallpaperTile wallpaperInfo = mWallpapers.get(position);
         wallpaperInfo.setView(view);
         ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image);
@@ -111,8 +110,8 @@
     }
 
     public static class LiveWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo {
-        private Drawable mThumbnail;
-        private WallpaperInfo mInfo;
+        @Thunk Drawable mThumbnail;
+        @Thunk WallpaperInfo mInfo;
         public LiveWallpaperTile(Drawable thumbnail, WallpaperInfo info, Intent intent) {
             mThumbnail = thumbnail;
             mInfo = info;
@@ -122,8 +121,8 @@
             Intent preview = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER);
             preview.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
                     mInfo.getComponent());
-            a.onLiveWallpaperPickerLaunch(mInfo);
-            a.startActivityForResultSafely(preview, WallpaperPickerActivity.PICK_LIVE_WALLPAPER);
+            a.startActivityForResultSafely(preview,
+                    WallpaperPickerActivity.PICK_WALLPAPER_THIRD_PARTY_ACTIVITY);
         }
     }
 
diff --git a/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java
index 9f92bc1..64b0ac4 100644
--- a/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java
+++ b/WallpaperPicker/src/com/android/launcher3/SavedWallpaperImages.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import android.app.Activity;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
@@ -60,13 +59,13 @@
         }
     }
 
-    public SavedWallpaperImages(Activity context) {
+    public SavedWallpaperImages(Context context) {
         // We used to store the saved images in the cache directory, but that meant they'd get
         // deleted sometimes-- move them to the data directory
         ImageDb.moveFromCacheDirectoryIfNecessary(context);
         mDb = new ImageDb(context);
         mContext = context;
-        mLayoutInflater = context.getLayoutInflater();
+        mLayoutInflater = LayoutInflater.from(context);
     }
 
     public void loadThumbnailsAndImageIdList() {
diff --git a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java
index 7a4d48c..f46da53 100644
--- a/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java
+++ b/WallpaperPicker/src/com/android/launcher3/ThirdPartyWallpaperPickerListAdapter.java
@@ -28,16 +28,15 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
-import android.widget.FrameLayout;
 import android.widget.ListAdapter;
 import android.widget.TextView;
 
+import com.android.launcher3.util.Thunk;
+
 import java.util.ArrayList;
 import java.util.List;
 
 public class ThirdPartyWallpaperPickerListAdapter extends BaseAdapter implements ListAdapter {
-    private static final String LOG_TAG = "LiveWallpaperListAdapter";
-
     private final LayoutInflater mInflater;
     private final PackageManager mPackageManager;
     private final int mIconSize;
@@ -46,7 +45,7 @@
             new ArrayList<ThirdPartyWallpaperTile>();
 
     public static class ThirdPartyWallpaperTile extends WallpaperPickerActivity.WallpaperTileInfo {
-        private ResolveInfo mResolveInfo;
+        @Thunk ResolveInfo mResolveInfo;
         public ThirdPartyWallpaperTile(ResolveInfo resolveInfo) {
             mResolveInfo = resolveInfo;
         }
@@ -62,7 +61,7 @@
     }
 
     public ThirdPartyWallpaperPickerListAdapter(Context context) {
-        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mInflater = LayoutInflater.from(context);
         mPackageManager = context.getPackageManager();
         mIconSize = context.getResources().getDimensionPixelSize(R.dimen.wallpaperItemIconSize);
         final PackageManager pm = mPackageManager;
@@ -126,8 +125,6 @@
             view = convertView;
         }
 
-        WallpaperPickerActivity.setWallpaperItemPaddingToZero((FrameLayout) view);
-
         ResolveInfo info = mThirdPartyWallpaperPickers.get(position).mResolveInfo;
         TextView label = (TextView) view.findViewById(R.id.wallpaper_item_label);
         label.setText(info.loadLabel(mPackageManager));
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
index fa8ec64..142a9cb 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;
@@ -25,42 +26,41 @@
 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.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
 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.launcher3.base.BaseActivity;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.WallpaperUtils;
 import com.android.photos.BitmapRegionTileSource;
 import com.android.photos.BitmapRegionTileSource.BitmapSource;
+import com.android.photos.BitmapRegionTileSource.BitmapSource.InBitmapProvider;
+import com.android.photos.views.TiledImageRenderer.TileSource;
 
-import java.io.BufferedInputStream;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
 
-public class WallpaperCropActivity extends Activity {
+public class WallpaperCropActivity extends BaseActivity implements Handler.Callback {
     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;
+    protected static final String WALLPAPER_WIDTH_KEY = WallpaperUtils.WALLPAPER_WIDTH_KEY;
+    protected static final String WALLPAPER_HEIGHT_KEY = WallpaperUtils.WALLPAPER_HEIGHT_KEY;
+
     /**
      * 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,17 +69,31 @@
      * 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;
+    public static final float WALLPAPER_SCREENS_SPAN = WallpaperUtils.WALLPAPER_SCREENS_SPAN;
 
-    protected static Point sDefaultWallpaperSize;
+    private static final int MSG_LOAD_IMAGE = 1;
 
     protected CropView mCropView;
+    protected View mProgressView;
     protected Uri mUri;
     protected View mSetWallpaperButton;
 
+    private HandlerThread mLoaderThread;
+    private Handler mLoaderHandler;
+    @Thunk LoadRequest mCurrentLoadRequest;
+    private byte[] mTempStorageForDecoding = new byte[16 * 1024];
+    // A weak-set of reusable bitmaps
+    @Thunk Set<Bitmap> mReusableBitmaps =
+            Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
+
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
+    public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        mLoaderThread = new HandlerThread("wallpaper_loader");
+        mLoaderThread.start();
+        mLoaderHandler = new Handler(mLoaderThread.getLooper(), this);
+
         init();
         if (!enableRotation()) {
             setRequestedOrientation(Configuration.ORIENTATION_PORTRAIT);
@@ -90,6 +104,7 @@
         setContentView(R.layout.wallpaper_cropper);
 
         mCropView = (CropView) findViewById(R.id.cropView);
+        mProgressView = findViewById(R.id.loading);
 
         Intent cropIntent = getIntent();
         final Uri imageUri = cropIntent.getData();
@@ -116,13 +131,12 @@
 
         // Load image in background
         final BitmapRegionTileSource.UriBitmapSource bitmapSource =
-                new BitmapRegionTileSource.UriBitmapSource(this, imageUri, 1024);
+                new BitmapRegionTileSource.UriBitmapSource(getContext(), imageUri, 1024);
         mSetWallpaperButton.setEnabled(false);
         Runnable onLoad = new Runnable() {
             public void run() {
                 if (bitmapSource.getLoadingState() != BitmapSource.State.LOADED) {
-                    Toast.makeText(WallpaperCropActivity.this,
-                            getString(R.string.wallpaper_load_fail),
+                    Toast.makeText(getContext(), R.string.wallpaper_load_fail,
                             Toast.LENGTH_LONG).show();
                     finish();
                 } else {
@@ -130,188 +144,156 @@
                 }
             }
         };
-        setCropViewTileSource(bitmapSource, true, false, onLoad);
+        setCropViewTileSource(bitmapSource, true, false, null, onLoad);
     }
 
     @Override
-    protected void onDestroy() {
+    public void onDestroy() {
         if (mCropView != null) {
             mCropView.destroy();
         }
+        if (mLoaderThread != null) {
+            mLoaderThread.quit();
+        }
         super.onDestroy();
     }
 
-    public void setCropViewTileSource(
-            final BitmapRegionTileSource.BitmapSource bitmapSource, final boolean touchEnabled,
-            final boolean moveToLeft, final Runnable postExecute) {
-        final Context context = WallpaperCropActivity.this;
-        final View progressView = findViewById(R.id.loading);
-        final AsyncTask<Void, Void, Void> loadBitmapTask = new AsyncTask<Void, Void, Void>() {
-            protected Void doInBackground(Void...args) {
-                if (!isCancelled()) {
-                    try {
-                        bitmapSource.loadInBackground();
-                    } catch (SecurityException securityException) {
-                        if (isDestroyed()) {
-                            // Temporarily granted permissions are revoked when the activity
-                            // finishes, potentially resulting in a SecurityException here.
-                            // Even though {@link #isDestroyed} might also return true in different
-                            // situations where the configuration changes, we are fine with
-                            // catching these cases here as well.
-                            cancel(false);
-                        } else {
-                            // otherwise it had a different cause and we throw it further
-                            throw securityException;
+    /**
+     * This is called on {@link #mLoaderThread}
+     */
+    @Override
+    public boolean handleMessage(Message msg) {
+        if (msg.what == MSG_LOAD_IMAGE) {
+            final LoadRequest req = (LoadRequest) msg.obj;
+            try {
+                req.src.loadInBackground(new InBitmapProvider() {
+
+                    @Override
+                    public Bitmap forPixelCount(int count) {
+                        Bitmap bitmapToReuse = null;
+                        // Find the smallest bitmap that satisfies the pixel count limit
+                        synchronized (mReusableBitmaps) {
+                            int currentBitmapSize = Integer.MAX_VALUE;
+                            for (Bitmap b : mReusableBitmaps) {
+                                int bitmapSize = b.getWidth() * b.getHeight();
+                                if ((bitmapSize >= count) && (bitmapSize < currentBitmapSize)) {
+                                    bitmapToReuse = b;
+                                    currentBitmapSize = bitmapSize;
+                                }
+                            }
+
+                            if (bitmapToReuse != null) {
+                                mReusableBitmaps.remove(bitmapToReuse);
+                            }
                         }
+                        return bitmapToReuse;
                     }
-                }
-                return null;
-            }
-            protected void onPostExecute(Void arg) {
-                if (!isCancelled()) {
-                    progressView.setVisibility(View.INVISIBLE);
-                    if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
-                        mCropView.setTileSource(
-                                new BitmapRegionTileSource(context, bitmapSource), null);
-                        mCropView.setTouchEnabled(touchEnabled);
-                        if (moveToLeft) {
-                            mCropView.moveToLeft();
-                        }
-                    }
-                }
-                if (postExecute != null) {
-                    postExecute.run();
+                });
+            } catch (SecurityException securityException) {
+                if (isDestroyed()) {
+                    // Temporarily granted permissions are revoked when the activity
+                    // finishes, potentially resulting in a SecurityException here.
+                    // Even though {@link #isDestroyed} might also return true in different
+                    // situations where the configuration changes, we are fine with
+                    // catching these cases here as well.
+                    return true;
+                } else {
+                    // otherwise it had a different cause and we throw it further
+                    throw securityException;
                 }
             }
-        };
+
+            req.result = new BitmapRegionTileSource(getContext(), req.src, mTempStorageForDecoding);
+            runOnUiThread(new Runnable() {
+
+                @Override
+                public void run() {
+                    if (req == mCurrentLoadRequest) {
+                        onLoadRequestComplete(req,
+                                req.src.getLoadingState() == BitmapSource.State.LOADED);
+                    } else {
+                        addReusableBitmap(req.result);
+                    }
+                }
+            });
+            return true;
+        }
+        return false;
+    }
+
+    @Thunk void addReusableBitmap(TileSource src) {
+        synchronized (mReusableBitmaps) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
+                    && src instanceof BitmapRegionTileSource) {
+                Bitmap preview = ((BitmapRegionTileSource) src).getBitmap();
+                if (preview != null && preview.isMutable()) {
+                    mReusableBitmaps.add(preview);
+                }
+            }
+        }
+    }
+
+    protected void onLoadRequestComplete(LoadRequest req, boolean success) {
+        mCurrentLoadRequest = null;
+        if (success) {
+            TileSource oldSrc = mCropView.getTileSource();
+            mCropView.setTileSource(req.result, null);
+            mCropView.setTouchEnabled(req.touchEnabled);
+            if (req.moveToLeft) {
+                mCropView.moveToLeft();
+            }
+            if (req.scaleProvider != null) {
+                mCropView.setScale(req.scaleProvider.getScale(req.result));
+            }
+
+            // Free last image
+            if (oldSrc != null) {
+                // Call yield instead of recycle, as we only want to free GL resource.
+                // We can still reuse the bitmap for decoding any other image.
+                oldSrc.getPreview().yield();
+            }
+            addReusableBitmap(oldSrc);
+        }
+        if (req.postExecute != null) {
+            req.postExecute.run();
+        }
+    }
+
+    public final void setCropViewTileSource(BitmapSource bitmapSource, boolean touchEnabled,
+            boolean moveToLeft, CropViewScaleProvider scaleProvider, Runnable postExecute) {
+        final LoadRequest req = new LoadRequest();
+        req.moveToLeft = moveToLeft;
+        req.src = bitmapSource;
+        req.touchEnabled = touchEnabled;
+        req.postExecute = postExecute;
+        req.scaleProvider = scaleProvider;
+        mCurrentLoadRequest = req;
+
+        // Remove any pending requests
+        mLoaderHandler.removeMessages(MSG_LOAD_IMAGE);
+        Message.obtain(mLoaderHandler, MSG_LOAD_IMAGE, req).sendToTarget();
+
         // We don't want to show the spinner every time we load an image, because that would be
         // annoying; instead, only start showing the spinner if loading the image has taken
         // longer than 1 sec (ie 1000 ms)
-        progressView.postDelayed(new Runnable() {
+        mProgressView.postDelayed(new Runnable() {
             public void run() {
-                if (loadBitmapTask.getStatus() != AsyncTask.Status.FINISHED) {
-                    progressView.setVisibility(View.VISIBLE);
+                if (mCurrentLoadRequest == req) {
+                    mProgressView.setVisibility(View.VISIBLE);
                 }
             }
         }, 1000);
-        loadBitmapTask.execute();
     }
 
+
     public boolean enableRotation() {
         return getResources().getBoolean(R.bool.allow_rotation);
     }
 
-    public static String getSharedPreferencesKey() {
-        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(getContext(), uri);
         BitmapCropTask cropTask = new BitmapCropTask(
-                this, uri, null, rotation, 0, 0, true, false, null);
+                getContext(), uri, null, rotation, 0, 0, true, false, null);
         final Point bounds = cropTask.getImageBounds();
         Runnable onEndCrop = new Runnable() {
             public void run() {
@@ -331,11 +313,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 = WallpaperUtils.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() {
@@ -348,18 +330,14 @@
                 }
             }
         };
-        BitmapCropTask cropTask = new BitmapCropTask(this, res, resId,
+        BitmapCropTask cropTask = new BitmapCropTask(getContext(), res, resId,
                 crop, rotation, outSize.x, outSize.y, true, false, onEndCrop);
         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 +348,7 @@
         d.getSize(displaySize);
         boolean isPortrait = displaySize.x < displaySize.y;
 
-        Point defaultWallpaperSize = getDefaultWallpaperSize(getResources(),
+        Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(getResources(),
                 getWindowManager());
         // Get the crop
         RectF cropRect = mCropView.getCrop();
@@ -444,7 +422,7 @@
                 }
             }
         };
-        BitmapCropTask cropTask = new BitmapCropTask(this, uri,
+        BitmapCropTask cropTask = new BitmapCropTask(getContext(), uri,
                 cropRect, cropRotation, outWidth, outHeight, true, false, onEndCrop);
         if (onBitmapCroppedHandler != null) {
             cropTask.setOnBitmapCropped(onBitmapCroppedHandler);
@@ -452,375 +430,9 @@
         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);
+        String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY;
+        SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
         SharedPreferences.Editor editor = sp.edit();
         if (width != 0 && height != 0) {
             editor.putInt(WALLPAPER_WIDTH_KEY, width);
@@ -830,69 +442,21 @@
             editor.remove(WALLPAPER_HEIGHT_KEY);
         }
         editor.commit();
-
-        suggestWallpaperDimension(getResources(),
-                sp, getWindowManager(), WallpaperManager.getInstance(this), true);
+        WallpaperUtils.suggestWallpaperDimension(getResources(),
+                sp, getWindowManager(), WallpaperManager.getInstance(getContext()), true);
     }
 
-    static public void suggestWallpaperDimension(Resources res,
-            final SharedPreferences sharedPrefs,
-            WindowManager windowManager,
-            final WallpaperManager wallpaperManager, boolean fallBackToDefaults) {
-        final Point defaultWallpaperSize = getDefaultWallpaperSize(res, windowManager);
-        // If we have saved a wallpaper width/height, use that instead
+    static class LoadRequest {
+        BitmapSource src;
+        boolean touchEnabled;
+        boolean moveToLeft;
+        Runnable postExecute;
+        CropViewScaleProvider scaleProvider;
 
-        int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1);
-        int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1);
-
-        if (savedWidth == -1 || savedHeight == -1) {
-            if (!fallBackToDefaults) {
-                return;
-            } else {
-                savedWidth = defaultWallpaperSize.x;
-                savedHeight = defaultWallpaperSize.y;
-            }
-        }
-
-        if (savedWidth != wallpaperManager.getDesiredMinimumWidth() ||
-                savedHeight != wallpaperManager.getDesiredMinimumHeight()) {
-            wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
-        }
+        TileSource result;
     }
 
-    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";
+    interface CropViewScaleProvider {
+        float getScale(TileSource src);
     }
 }
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
index 09e0963..9332091 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
@@ -20,8 +20,8 @@
 import android.annotation.TargetApi;
 import android.app.ActionBar;
 import android.app.Activity;
-import android.app.WallpaperInfo;
 import android.app.WallpaperManager;
+import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -35,11 +35,9 @@
 import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.PorterDuff;
-import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LevelListDrawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Build;
@@ -70,8 +68,14 @@
 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.launcher3.util.Thunk;
+import com.android.launcher3.util.WallpaperUtils;
 import com.android.photos.BitmapRegionTileSource;
 import com.android.photos.BitmapRegionTileSource.BitmapSource;
+import com.android.photos.views.TiledImageRenderer.TileSource;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -83,28 +87,25 @@
 
     public static final int IMAGE_PICK = 5;
     public static final int PICK_WALLPAPER_THIRD_PARTY_ACTIVITY = 6;
-    public static final int PICK_LIVE_WALLPAPER = 7;
     private static final String TEMP_WALLPAPER_TILES = "TEMP_WALLPAPER_TILES";
     private static final String SELECTED_INDEX = "SELECTED_INDEX";
     private static final int FLAG_POST_DELAY_MILLIS = 200;
 
-    private View mSelectedTile;
-    private boolean mIgnoreNextTap;
-    private OnClickListener mThumbnailOnClickListener;
+    @Thunk View mSelectedTile;
+    @Thunk boolean mIgnoreNextTap;
+    @Thunk OnClickListener mThumbnailOnClickListener;
 
-    private LinearLayout mWallpapersView;
-    private View mWallpaperStrip;
+    @Thunk LinearLayout mWallpapersView;
+    @Thunk HorizontalScrollView mWallpaperScrollContainer;
 
-    private ActionMode.Callback mActionModeCallback;
-    private ActionMode mActionMode;
+    @Thunk ActionMode.Callback mActionModeCallback;
+    @Thunk ActionMode mActionMode;
 
-    private View.OnLongClickListener mLongClickListener;
+    @Thunk View.OnLongClickListener mLongClickListener;
 
     ArrayList<Uri> mTempWallpaperTiles = new ArrayList<Uri>();
     private SavedWallpaperImages mSavedImages;
-    private WallpaperInfo mLiveWallpaperInfoOnPickerLaunch;
-    private int mSelectedIndex = -1;
-    private WallpaperInfo mLastClickedLiveWallpaperInfo;
+    @Thunk int mSelectedIndex = -1;
 
     public static abstract class WallpaperTileInfo {
         protected View mView;
@@ -136,45 +137,37 @@
 
     public static class UriWallpaperInfo extends WallpaperTileInfo {
         private Uri mUri;
-        private boolean mFirstClick = true;
-        private BitmapRegionTileSource.UriBitmapSource mBitmapSource;
         public UriWallpaperInfo(Uri uri) {
             mUri = uri;
         }
         @Override
         public void onClick(final WallpaperPickerActivity a) {
-            final Runnable onLoad;
-            if (!mFirstClick) {
-                onLoad = null;
-            } else {
-                mFirstClick = false;
-                a.mSetWallpaperButton.setEnabled(false);
-                onLoad = new Runnable() {
-                    public void run() {
-                        if (mBitmapSource != null &&
-                                mBitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
-                            a.selectTile(mView);
-                            a.mSetWallpaperButton.setEnabled(true);
-                        } else {
-                            ViewGroup parent = (ViewGroup) mView.getParent();
-                            if (parent != null) {
-                                parent.removeView(mView);
-                                Toast.makeText(a,
-                                        a.getString(R.string.image_load_fail),
-                                        Toast.LENGTH_SHORT).show();
-                            }
+            a.setWallpaperButtonEnabled(false);
+            final BitmapRegionTileSource.UriBitmapSource bitmapSource =
+                    new BitmapRegionTileSource.UriBitmapSource(
+                            a.getContext(), mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE);
+            a.setCropViewTileSource(bitmapSource, true, false, null, new Runnable() {
+
+                @Override
+                public void run() {
+                    if (bitmapSource.getLoadingState() == BitmapSource.State.LOADED) {
+                        a.selectTile(mView);
+                        a.setWallpaperButtonEnabled(true);
+                    } else {
+                        ViewGroup parent = (ViewGroup) mView.getParent();
+                        if (parent != null) {
+                            parent.removeView(mView);
+                            Toast.makeText(a.getContext(), R.string.image_load_fail,
+                                    Toast.LENGTH_SHORT).show();
                         }
                     }
-                };
-            }
-            mBitmapSource = new BitmapRegionTileSource.UriBitmapSource(
-                    a, mUri, BitmapRegionTileSource.MAX_PREVIEW_SIZE);
-            a.setCropViewTileSource(mBitmapSource, true, false, onLoad);
+                }
+            });
         }
         @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
@@ -203,10 +196,18 @@
             mThumb = thumb;
         }
         @Override
-        public void onClick(WallpaperPickerActivity a) {
+        public void onClick(final WallpaperPickerActivity a) {
+            a.setWallpaperButtonEnabled(false);
             BitmapRegionTileSource.UriBitmapSource bitmapSource =
-                    new BitmapRegionTileSource.UriBitmapSource(a, Uri.fromFile(mFile), 1024);
-            a.setCropViewTileSource(bitmapSource, false, true, null);
+                    new BitmapRegionTileSource.UriBitmapSource(a.getContext(),
+                            Uri.fromFile(mFile), 1024);
+            a.setCropViewTileSource(bitmapSource, false, true, null, new Runnable() {
+
+                @Override
+                public void run() {
+                    a.setWallpaperButtonEnabled(true);
+                }
+            });
         }
         @Override
         public void onSave(WallpaperPickerActivity a) {
@@ -232,22 +233,29 @@
             mThumb = thumb;
         }
         @Override
-        public void onClick(WallpaperPickerActivity a) {
+        public void onClick(final WallpaperPickerActivity a) {
+            a.setWallpaperButtonEnabled(false);
             BitmapRegionTileSource.ResourceBitmapSource bitmapSource =
                     new BitmapRegionTileSource.ResourceBitmapSource(
                             mResources, mResId, BitmapRegionTileSource.MAX_PREVIEW_SIZE);
-            bitmapSource.loadInBackground();
-            BitmapRegionTileSource source = new BitmapRegionTileSource(a, bitmapSource);
-            CropView v = a.getCropView();
-            v.setTileSource(source, null);
-            Point wallpaperSize = WallpaperCropActivity.getDefaultWallpaperSize(
-                    a.getResources(), a.getWindowManager());
-            RectF crop = WallpaperCropActivity.getMaxCropRect(
-                    source.getImageWidth(), source.getImageHeight(),
-                    wallpaperSize.x, wallpaperSize.y, false);
-            v.setScale(wallpaperSize.x / crop.width());
-            v.setTouchEnabled(false);
-            a.setSystemWallpaperVisiblity(false);
+            a.setCropViewTileSource(bitmapSource, false, false, new CropViewScaleProvider() {
+
+                @Override
+                public float getScale(TileSource src) {
+                    Point wallpaperSize = WallpaperUtils.getDefaultWallpaperSize(
+                            a.getResources(), a.getWindowManager());
+                    RectF crop = Utils.getMaxCropRect(
+                            src.getImageWidth(), src.getImageHeight(),
+                            wallpaperSize.x, wallpaperSize.y, false);
+                    return wallpaperSize.x / crop.width();
+                }
+            }, new Runnable() {
+
+                @Override
+                public void run() {
+                    a.setWallpaperButtonEnabled(true);
+                }
+            });
         }
         @Override
         public void onSave(WallpaperPickerActivity a) {
@@ -272,27 +280,33 @@
         @Override
         public void onClick(WallpaperPickerActivity a) {
             CropView c = a.getCropView();
-
-            Drawable defaultWallpaper = WallpaperManager.getInstance(a).getBuiltInDrawable(
-                    c.getWidth(), c.getHeight(), false, 0.5f, 0.5f);
-
+            Drawable defaultWallpaper = WallpaperManager.getInstance(a.getContext())
+                    .getBuiltInDrawable(c.getWidth(), c.getHeight(), false, 0.5f, 0.5f);
             if (defaultWallpaper == null) {
                 Log.w(TAG, "Null default wallpaper encountered.");
                 c.setTileSource(null, null);
                 return;
             }
 
-            c.setTileSource(
-                    new DrawableTileSource(a, defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE), null);
-            c.setScale(1f);
-            c.setTouchEnabled(false);
-            a.setSystemWallpaperVisiblity(false);
+            LoadRequest req = new LoadRequest();
+            req.moveToLeft = false;
+            req.touchEnabled = false;
+            req.scaleProvider = new CropViewScaleProvider() {
+
+                @Override
+                public float getScale(TileSource src) {
+                    return 1f;
+                }
+            };
+            req.result = new DrawableTileSource(a.getContext(),
+                    defaultWallpaper, DrawableTileSource.MAX_PREVIEW_SIZE);
+            a.onLoadRequestComplete(req, true);
         }
         @Override
         public void onSave(WallpaperPickerActivity a) {
             try {
-                WallpaperManager.getInstance(a).clear();
-                a.setResult(RESULT_OK);
+                WallpaperManager.getInstance(a.getContext()).clear();
+                a.setResult(Activity.RESULT_OK);
             } catch (IOException e) {
                 Log.w("Setting wallpaper to default threw exception", e);
             }
@@ -308,10 +322,6 @@
         }
     }
 
-    public void setWallpaperStripYOffset(float offset) {
-        mWallpaperStrip.setPadding(0, 0, 0, (int) offset);
-    }
-
     /**
      * shows the system wallpaper behind the window and hides the {@link
      * #mCropView} if visible
@@ -338,7 +348,7 @@
         }, FLAG_POST_DELAY_MILLIS);
     }
 
-    private void changeWallpaperFlags(boolean visible) {
+    @Thunk void changeWallpaperFlags(boolean visible) {
         int desiredWallpaperFlag = visible ? WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER : 0;
         int currentWallpaperFlag = getWindow().getAttributes().flags
                 & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
@@ -349,24 +359,11 @@
     }
 
     @Override
-    public void setCropViewTileSource(BitmapSource bitmapSource,
-                                      boolean touchEnabled,
-                                      boolean moveToLeft,
-                                      final Runnable postExecute) {
-        // we also want to show our own wallpaper instead of the one in the background
-        Runnable showPostExecuteRunnable = new Runnable() {
-            @Override
-            public void run() {
-                if(postExecute != null) {
-                    postExecute.run();
-                }
-                setSystemWallpaperVisiblity(false);
-            }
-        };
-        super.setCropViewTileSource(bitmapSource,
-                touchEnabled,
-                moveToLeft,
-                showPostExecuteRunnable);
+    protected void onLoadRequestComplete(LoadRequest req, boolean success) {
+        super.onLoadRequestComplete(req, success);
+        if (success) {
+            setSystemWallpaperVisiblity(false);
+        }
     }
 
     // called by onCreate; this is subclassed to overwrite WallpaperCropActivity
@@ -376,7 +373,8 @@
         mCropView = (CropView) findViewById(R.id.cropView);
         mCropView.setVisibility(View.INVISIBLE);
 
-        mWallpaperStrip = findViewById(R.id.wallpaper_strip);
+        mProgressView = findViewById(R.id.loading);
+        mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container);
         mCropView.setTouchCallback(new CropView.TouchCallback() {
             ViewPropertyAnimator mAnim;
             @Override
@@ -384,15 +382,15 @@
                 if (mAnim != null) {
                     mAnim.cancel();
                 }
-                if (mWallpaperStrip.getAlpha() == 1f) {
+                if (mWallpaperScrollContainer.getAlpha() == 1f) {
                     mIgnoreNextTap = true;
                 }
-                mAnim = mWallpaperStrip.animate();
+                mAnim = mWallpaperScrollContainer.animate();
                 mAnim.alpha(0f)
                     .setDuration(150)
                     .withEndAction(new Runnable() {
                         public void run() {
-                            mWallpaperStrip.setVisibility(View.INVISIBLE);
+                            mWallpaperScrollContainer.setVisibility(View.INVISIBLE);
                         }
                     });
                 mAnim.setInterpolator(new AccelerateInterpolator(0.75f));
@@ -410,8 +408,8 @@
                     if (mAnim != null) {
                         mAnim.cancel();
                     }
-                    mWallpaperStrip.setVisibility(View.VISIBLE);
-                    mAnim = mWallpaperStrip.animate();
+                    mWallpaperScrollContainer.setVisibility(View.VISIBLE);
+                    mAnim = mWallpaperScrollContainer.animate();
                     mAnim.alpha(1f)
                          .setDuration(150)
                          .setInterpolator(new DecelerateInterpolator(0.75f));
@@ -429,7 +427,7 @@
                     }
                     return;
                 }
-                mSetWallpaperButton.setEnabled(true);
+                setWallpaperButtonEnabled(true);
                 WallpaperTileInfo info = (WallpaperTileInfo) v.getTag();
                 if (info.isSelectable() && v.getVisibility() == View.VISIBLE) {
                     selectTile(v);
@@ -460,18 +458,18 @@
         // Populate the built-in wallpapers
         ArrayList<WallpaperTileInfo> wallpapers = findBundledWallpapers();
         mWallpapersView = (LinearLayout) findViewById(R.id.wallpaper_list);
-        SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(this, wallpapers);
+        SimpleWallpapersAdapter ia = new SimpleWallpapersAdapter(getContext(), wallpapers);
         populateWallpapersFromAdapter(mWallpapersView, ia, false);
 
         // Populate the saved wallpapers
-        mSavedImages = new SavedWallpaperImages(this);
+        mSavedImages = new SavedWallpaperImages(getContext());
         mSavedImages.loadThumbnailsAndImageIdList();
         populateWallpapersFromAdapter(mWallpapersView, mSavedImages, true);
 
         // Populate the live wallpapers
         final LinearLayout liveWallpapersView =
                 (LinearLayout) findViewById(R.id.live_wallpaper_list);
-        final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(this);
+        final LiveWallpaperListAdapter a = new LiveWallpaperListAdapter(getContext());
         a.registerDataSetObserver(new DataSetObserver() {
             public void onChanged() {
                 liveWallpapersView.removeAllViews();
@@ -485,14 +483,13 @@
         final LinearLayout thirdPartyWallpapersView =
                 (LinearLayout) findViewById(R.id.third_party_wallpaper_list);
         final ThirdPartyWallpaperPickerListAdapter ta =
-                new ThirdPartyWallpaperPickerListAdapter(this);
+                new ThirdPartyWallpaperPickerListAdapter(getContext());
         populateWallpapersFromAdapter(thirdPartyWallpapersView, ta, false);
 
         // Add a tile for the Gallery
         LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list);
         FrameLayout pickImageTile = (FrameLayout) getLayoutInflater().
                 inflate(R.layout.wallpaper_picker_image_picker_item, masterWallpaperList, false);
-        setWallpaperItemPaddingToZero(pickImageTile);
         masterWallpaperList.addView(pickImageTile, 0);
 
         // Make its background the last photo taken on external storage
@@ -500,10 +497,9 @@
         if (lastPhoto != null) {
             ImageView galleryThumbnailBg =
                     (ImageView) pickImageTile.findViewById(R.id.wallpaper_image);
-            galleryThumbnailBg.setImageBitmap(getThumbnailOfLastPhoto());
+            galleryThumbnailBg.setImageBitmap(lastPhoto);
             int colorOverlay = getResources().getColor(R.color.wallpaper_picker_translucent_gray);
             galleryThumbnailBg.setColorFilter(colorOverlay, PorterDuff.Mode.SRC_ATOP);
-
         }
 
         PickImageInfo pickImageInfo = new PickImageInfo();
@@ -650,7 +646,11 @@
         };
     }
 
-    private void selectTile(View v) {
+    public void setWallpaperButtonEnabled(boolean enabled) {
+        mSetWallpaperButton.setEnabled(enabled);
+    }
+
+    @Thunk void selectTile(View v) {
         if (mSelectedTile != null) {
             mSelectedTile.setSelected(false);
             mSelectedTile = null;
@@ -661,28 +661,25 @@
         // TODO: Remove this once the accessibility framework and
         // services have better support for selection state.
         v.announceForAccessibility(
-                getString(R.string.announce_selection, v.getContentDescription()));
+                getContext().getString(R.string.announce_selection, v.getContentDescription()));
     }
 
-    private void initializeScrollForRtl() {
-        final HorizontalScrollView scroll =
-                (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container);
-
-        if (scroll.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            final ViewTreeObserver observer = scroll.getViewTreeObserver();
+    @Thunk void initializeScrollForRtl() {
+        if (mWallpaperScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+            final ViewTreeObserver observer = mWallpaperScrollContainer.getViewTreeObserver();
             observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
                 public void onGlobalLayout() {
                     LinearLayout masterWallpaperList =
                             (LinearLayout) findViewById(R.id.master_wallpaper_list);
-                    scroll.scrollTo(masterWallpaperList.getWidth(), 0);
-                    scroll.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                    mWallpaperScrollContainer.scrollTo(masterWallpaperList.getWidth(), 0);
+                    mWallpaperScrollContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                 }
             });
         }
     }
 
     protected Bitmap getThumbnailOfLastPhoto() {
-        Cursor cursor = MediaStore.Images.Media.query(getContentResolver(),
+        Cursor cursor = MediaStore.Images.Media.query(getContext().getContentResolver(),
                 MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                 new String[] { MediaStore.Images.ImageColumns._ID,
                     MediaStore.Images.ImageColumns.DATE_TAKEN},
@@ -692,7 +689,7 @@
         if (cursor != null) {
             if (cursor.moveToNext()) {
                 int id = cursor.getInt(0);
-                thumb = MediaStore.Images.Thumbnails.getThumbnail(getContentResolver(),
+                thumb = MediaStore.Images.Thumbnails.getThumbnail(getContext().getContentResolver(),
                         id, MediaStore.Images.Thumbnails.MINI_KIND, null);
             }
             cursor.close();
@@ -700,16 +697,16 @@
         return thumb;
     }
 
-    protected void onStop() {
+    public void onStop() {
         super.onStop();
-        mWallpaperStrip = findViewById(R.id.wallpaper_strip);
-        if (mWallpaperStrip.getAlpha() < 1f) {
-            mWallpaperStrip.setAlpha(1f);
-            mWallpaperStrip.setVisibility(View.VISIBLE);
+        mWallpaperScrollContainer = (HorizontalScrollView) findViewById(R.id.wallpaper_scroll_container);
+        if (mWallpaperScrollContainer.getAlpha() < 1f) {
+            mWallpaperScrollContainer.setAlpha(1f);
+            mWallpaperScrollContainer.setVisibility(View.VISIBLE);
         }
     }
 
-    protected void onSaveInstanceState(Bundle outState) {
+    public void onSaveInstanceState(Bundle outState) {
         outState.putParcelableArrayList(TEMP_WALLPAPER_TILES, mTempWallpaperTiles);
         outState.putInt(SELECTED_INDEX, mSelectedIndex);
     }
@@ -722,7 +719,7 @@
         mSelectedIndex = savedInstanceState.getInt(SELECTED_INDEX, -1);
     }
 
-    private void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter,
+    @Thunk void populateWallpapersFromAdapter(ViewGroup parent, BaseAdapter adapter,
             boolean addLongPressHandler) {
         for (int i = 0; i < adapter.getCount(); i++) {
             FrameLayout thumbnail = (FrameLayout) adapter.getView(i, null, parent);
@@ -737,7 +734,7 @@
         }
     }
 
-    private void updateTileIndices() {
+    @Thunk void updateTileIndices() {
         LinearLayout masterWallpaperList = (LinearLayout) findViewById(R.id.master_wallpaper_list);
         final int childCount = masterWallpaperList.getChildCount();
         final Resources res = getResources();
@@ -778,13 +775,13 @@
         }
     }
 
-    private static Point getDefaultThumbnailSize(Resources res) {
+    @Thunk static Point getDefaultThumbnailSize(Resources res) {
         return new Point(res.getDimensionPixelSize(R.dimen.wallpaperThumbnailWidth),
                 res.getDimensionPixelSize(R.dimen.wallpaperThumbnailHeight));
 
     }
 
-    private static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes,
+    @Thunk static Bitmap createThumbnail(Point size, Context context, Uri uri, byte[] imageBytes,
             Resources res, int resId, int rotation, boolean leftAligned) {
         int width = size.x;
         int height = size.y;
@@ -812,7 +809,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);
 
@@ -829,17 +826,16 @@
         final FrameLayout pickedImageThumbnail = (FrameLayout) getLayoutInflater().
                 inflate(R.layout.wallpaper_picker_item, mWallpapersView, false);
         pickedImageThumbnail.setVisibility(View.GONE);
-        setWallpaperItemPaddingToZero(pickedImageThumbnail);
         mWallpapersView.addView(pickedImageThumbnail, 0);
 
         // Load the thumbnail
         final ImageView image = (ImageView) pickedImageThumbnail.findViewById(R.id.wallpaper_image);
         final Point defaultSize = getDefaultThumbnailSize(this.getResources());
-        final Context context = this;
+        final Context context = getContext();
         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()) {
@@ -879,45 +875,26 @@
         }
     }
 
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == IMAGE_PICK && resultCode == RESULT_OK) {
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == IMAGE_PICK && resultCode == Activity.RESULT_OK) {
             if (data != null && data.getData() != null) {
                 Uri uri = data.getData();
                 addTemporaryWallpaperTile(uri, false);
             }
-        } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY) {
-            setResult(RESULT_OK);
+        } else if (requestCode == PICK_WALLPAPER_THIRD_PARTY_ACTIVITY
+                && resultCode == Activity.RESULT_OK) {
+            // Something was set on the third-party activity.
+            setResult(Activity.RESULT_OK);
             finish();
-        } else if (requestCode == PICK_LIVE_WALLPAPER) {
-            WallpaperManager wm = WallpaperManager.getInstance(this);
-            final WallpaperInfo oldLiveWallpaper = mLiveWallpaperInfoOnPickerLaunch;
-            final WallpaperInfo clickedWallpaper = mLastClickedLiveWallpaperInfo;
-            WallpaperInfo newLiveWallpaper = wm.getWallpaperInfo();
-            // Try to figure out if a live wallpaper was set;
-            if (newLiveWallpaper != null &&
-                    (oldLiveWallpaper == null
-                            || !oldLiveWallpaper.getComponent()
-                                    .equals(newLiveWallpaper.getComponent())
-                            || clickedWallpaper.getComponent()
-                                    .equals(oldLiveWallpaper.getComponent()))) {
-                // Return if a live wallpaper was set
-                setResult(RESULT_OK);
-                finish();
-            }
         }
     }
 
-    static void setWallpaperItemPaddingToZero(FrameLayout frameLayout) {
-        frameLayout.setPadding(0, 0, 0, 0);
-        frameLayout.setForeground(new ZeroPaddingDrawable(frameLayout.getForeground()));
-    }
-
     private void addLongPressHandler(View v) {
         v.setOnLongClickListener(mLongClickListener);
     }
 
     private ArrayList<WallpaperTileInfo> findBundledWallpapers() {
-        final PackageManager pm = getPackageManager();
+        final PackageManager pm = getContext().getPackageManager();
         final ArrayList<WallpaperTileInfo> bundled = new ArrayList<WallpaperTileInfo>(24);
 
         Partner partner = Partner.get(pm);
@@ -961,7 +938,8 @@
         Pair<ApplicationInfo, Integer> r = getWallpaperArrayResourceId();
         if (r != null) {
             try {
-                Resources wallpaperRes = getPackageManager().getResourcesForApplication(r.first);
+                Resources wallpaperRes = getContext().getPackageManager()
+                        .getResourcesForApplication(r.first);
                 addWallpapers(bundled, wallpaperRes, r.first.packageName, r.second);
             } catch (PackageManager.NameNotFoundException e) {
             }
@@ -984,7 +962,7 @@
         try {
             f.createNewFile();
             FileOutputStream thumbFileStream =
-                    openFileOutput(f.getName(), Context.MODE_PRIVATE);
+                    getContext().openFileOutput(f.getName(), Context.MODE_PRIVATE);
             b.compress(Bitmap.CompressFormat.JPEG, 95, thumbFileStream);
             thumbFileStream.close();
             return true;
@@ -996,17 +974,18 @@
     }
 
     private File getDefaultThumbFile() {
-        return new File(getFilesDir(), Build.VERSION.SDK_INT
+        return new File(getContext().getFilesDir(), Build.VERSION.SDK_INT
                 + "_" + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL);
     }
 
     private boolean saveDefaultWallpaperThumb(Bitmap b) {
         // Delete old thumbnails.
-        new File(getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete();
-        new File(getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete();
+        new File(getContext().getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL_OLD).delete();
+        new File(getContext().getFilesDir(), LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete();
 
         for (int i = Build.VERSION_CODES.JELLY_BEAN; i < Build.VERSION.SDK_INT; i++) {
-            new File(getFilesDir(), i + "_" + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete();
+            new File(getContext().getFilesDir(), i + "_"
+                    + LauncherFiles.DEFAULT_WALLPAPER_THUMBNAIL).delete();
         }
         return writeImageToFileAsJpeg(getDefaultThumbFile(), b);
     }
@@ -1024,9 +1003,9 @@
         } 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);
+                    defaultThumbSize, getContext(), null, null, sysRes, resId, rotation, false);
             if (thumb != null) {
                 defaultWallpaperExists = saveDefaultWallpaperThumb(thumb);
             }
@@ -1048,7 +1027,7 @@
         } else {
             Resources res = getResources();
             Point defaultThumbSize = getDefaultThumbnailSize(res);
-            Drawable wallpaperDrawable = WallpaperManager.getInstance(this).getBuiltInDrawable(
+            Drawable wallpaperDrawable = WallpaperManager.getInstance(getContext()).getBuiltInDrawable(
                     defaultThumbSize.x, defaultThumbSize.y, true, 0.5f, 0.5f);
             if (wallpaperDrawable != null) {
                 thumb = Bitmap.createBitmap(
@@ -1075,7 +1054,7 @@
         // package name should be.
         final String packageName = getResources().getResourcePackageName(R.array.wallpapers);
         try {
-            ApplicationInfo info = getPackageManager().getApplicationInfo(packageName, 0);
+            ApplicationInfo info = getContext().getPackageManager().getApplicationInfo(packageName, 0);
             return new Pair<ApplicationInfo, Integer>(info, R.array.wallpapers);
         } catch (PackageManager.NameNotFoundException e) {
             return null;
@@ -1110,31 +1089,12 @@
         return mSavedImages;
     }
 
-    public void onLiveWallpaperPickerLaunch(WallpaperInfo info) {
-        mLastClickedLiveWallpaperInfo = info;
-        mLiveWallpaperInfoOnPickerLaunch = WallpaperManager.getInstance(this).getWallpaperInfo();
-    }
-
-    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 static class SimpleWallpapersAdapter extends ArrayAdapter<WallpaperTileInfo> {
         private final LayoutInflater mLayoutInflater;
 
-        SimpleWallpapersAdapter(Activity activity, ArrayList<WallpaperTileInfo> wallpapers) {
-            super(activity, R.layout.wallpaper_picker_item, wallpapers);
-            mLayoutInflater = activity.getLayoutInflater();
+        SimpleWallpapersAdapter(Context context, ArrayList<WallpaperTileInfo> wallpapers) {
+            super(context, R.layout.wallpaper_picker_item, wallpapers);
+            mLayoutInflater = LayoutInflater.from(context);
         }
 
         public View getView(int position, View convertView, ViewGroup parent) {
@@ -1156,8 +1116,6 @@
             view = convertView;
         }
 
-        setWallpaperItemPaddingToZero((FrameLayout) view);
-
         ImageView image = (ImageView) view.findViewById(R.id.wallpaper_image);
 
         if (thumb != null) {
@@ -1168,9 +1126,12 @@
         return view;
     }
 
-    // In Launcher3, we override this with a method that catches exceptions
-    // from starting activities; didn't want to copy and paste code into here
     public void startActivityForResultSafely(Intent intent, int requestCode) {
-        startActivityForResult(intent, requestCode);
+        Utilities.startActivityForResultSafely(getActivity(), intent, requestCode);
+    }
+
+    @Override
+    public boolean enableRotation() {
+        return Utilities.isRotationEnabled(getContext());
     }
 }
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java b/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java
deleted file mode 100644
index ceaa043..0000000
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperRootView.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * 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.Rect;
-import android.util.AttributeSet;
-import android.widget.RelativeLayout;
-
-public class WallpaperRootView extends RelativeLayout {
-    private final WallpaperPickerActivity a;
-    public WallpaperRootView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        a = (WallpaperPickerActivity) context;
-    }
-    public WallpaperRootView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        a = (WallpaperPickerActivity) context;
-    }
-
-    protected boolean fitSystemWindows(Rect insets) {
-        a.setWallpaperStripYOffset(insets.bottom);
-        return true;
-    }
-}
diff --git a/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java b/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java
new file mode 100644
index 0000000..f854118
--- /dev/null
+++ b/WallpaperPicker/src/com/android/launcher3/base/BaseActivity.java
@@ -0,0 +1,21 @@
+package com.android.launcher3.base;
+
+import android.app.Activity;
+import android.content.Context;
+
+/**
+ * A wrapper over {@link Activity} which allows to override some methods.
+ * The base implementation can change from an Activity to a Fragment (or any other custom
+ * implementation), Callers should not assume that the base class extends Context, instead use
+ * either {@link #getContext} or {@link #getActivity}
+ */
+public class BaseActivity extends Activity {
+
+    public Context getContext() {
+        return this;
+    }
+
+    public Activity getActivity() {
+        return this;
+    }
+}
diff --git a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java
index 66ece4f..15f97e5 100644
--- a/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java
+++ b/WallpaperPicker/src/com/android/photos/BitmapRegionTileSource.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapRegionDecoder;
 import android.graphics.Canvas;
@@ -28,7 +27,6 @@
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Build;
-import android.os.Build.VERSION_CODES;
 import android.util.Log;
 
 import com.android.gallery3d.common.BitmapUtils;
@@ -148,8 +146,6 @@
 
     private static final String TAG = "BitmapRegionTileSource";
 
-    private static final boolean REUSE_BITMAP =
-            Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN;
     private static final int GL_SIZE_LIMIT = 2048;
     // This must be no larger than half the size of the GL_SIZE_LIMIT
     // due to decodePreview being allowed to be up to 2x the size of the target
@@ -158,14 +154,14 @@
     public static abstract class BitmapSource {
         private SimpleBitmapRegionDecoder mDecoder;
         private Bitmap mPreview;
-        private int mPreviewSize;
+        private final int mPreviewSize;
         private int mRotation;
         public enum State { NOT_LOADED, LOADED, ERROR_LOADING };
         private State mState = State.NOT_LOADED;
         public BitmapSource(int previewSize) {
-            mPreviewSize = previewSize;
+            mPreviewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
         }
-        public boolean loadInBackground() {
+        public boolean loadInBackground(InBitmapProvider bitmapProvider) {
             ExifInterface ei = new ExifInterface();
             if (readExif(ei)) {
                 Integer ori = ei.getTagIntValue(ExifInterface.TAG_ORIENTATION);
@@ -181,15 +177,33 @@
                 int width = mDecoder.getWidth();
                 int height = mDecoder.getHeight();
                 if (mPreviewSize != 0) {
-                    int previewSize = Math.min(mPreviewSize, MAX_PREVIEW_SIZE);
                     BitmapFactory.Options opts = new BitmapFactory.Options();
                     opts.inPreferredConfig = Bitmap.Config.ARGB_8888;
                     opts.inPreferQualityOverSpeed = true;
 
-                    float scale = (float) previewSize / Math.max(width, height);
+                    float scale = (float) mPreviewSize / Math.max(width, height);
                     opts.inSampleSize = BitmapUtils.computeSampleSizeLarger(scale);
                     opts.inJustDecodeBounds = false;
-                    mPreview = loadPreviewBitmap(opts);
+                    opts.inMutable = true;
+
+                    if (bitmapProvider != null) {
+                        int expectedPixles = (width / opts.inSampleSize) * (height / opts.inSampleSize);
+                        Bitmap reusableBitmap = bitmapProvider.forPixelCount(expectedPixles);
+                        if (reusableBitmap != null) {
+                            // Try loading with reusable bitmap
+                            opts.inBitmap = reusableBitmap;
+                            try {
+                                mPreview = loadPreviewBitmap(opts);
+                            } catch (IllegalArgumentException e) {
+                                Log.d(TAG, "Unable to reusage bitmap", e);
+                                opts.inBitmap = null;
+                                mPreview = null;
+                            }
+                        }
+                    }
+                    if (mPreview == null) {
+                        mPreview = loadPreviewBitmap(opts);
+                    }
                 }
                 mState = State.LOADED;
                 return true;
@@ -208,10 +222,6 @@
             return mPreview;
         }
 
-        public int getPreviewSize() {
-            return mPreviewSize;
-        }
-
         public int getRotation() {
             return mRotation;
         }
@@ -219,6 +229,10 @@
         public abstract boolean readExif(ExifInterface ei);
         public abstract SimpleBitmapRegionDecoder loadBitmapRegionDecoder();
         public abstract Bitmap loadPreviewBitmap(BitmapFactory.Options options);
+
+        public interface InBitmapProvider {
+            Bitmap forPixelCount(int count);
+        }
     }
 
     public static class FilePathBitmapSource extends BitmapSource {
@@ -306,13 +320,13 @@
                 Utils.closeSilently(is);
                 return true;
             } catch (FileNotFoundException e) {
-                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
+                Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                 return false;
             } catch (IOException e) {
-                Log.e("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
+                Log.d("BitmapRegionTileSource", "Failed to load URI " + mUri, e);
                 return false;
             } catch (NullPointerException e) {
-                Log.e("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
+                Log.d("BitmapRegionTileSource", "Failed to read EXIF for URI " + mUri, e);
                 return false;
             } finally {
                 Utils.closeSilently(is);
@@ -372,11 +386,9 @@
 
     // For use only by getTile
     private Rect mWantRegion = new Rect();
-    private Rect mOverlapRegion = new Rect();
     private BitmapFactory.Options mOptions;
-    private Canvas mCanvas;
 
-    public BitmapRegionTileSource(Context context, BitmapSource source) {
+    public BitmapRegionTileSource(Context context, BitmapSource source, byte[] tempStorage) {
         mTileSize = TiledImageRenderer.suggestedTileSize(context);
         mRotation = source.getRotation();
         mDecoder = source.getBitmapRegionDecoder();
@@ -386,27 +398,26 @@
             mOptions = new BitmapFactory.Options();
             mOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
             mOptions.inPreferQualityOverSpeed = true;
-            mOptions.inTempStorage = new byte[16 * 1024];
-            int previewSize = source.getPreviewSize();
-            if (previewSize != 0) {
-                previewSize = Math.min(previewSize, MAX_PREVIEW_SIZE);
-                // Although this is the same size as the Bitmap that is likely already
-                // loaded, the lifecycle is different and interactions are on a different
-                // thread. Thus to simplify, this source will decode its own bitmap.
-                Bitmap preview = decodePreview(source, previewSize);
-                if (preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
+            mOptions.inTempStorage = tempStorage;
+
+            Bitmap preview = source.getPreviewBitmap();
+            if (preview != null &&
+                    preview.getWidth() <= GL_SIZE_LIMIT && preview.getHeight() <= GL_SIZE_LIMIT) {
                     mPreview = new BitmapTexture(preview);
-                } else {
-                    Log.w(TAG, String.format(
-                            "Failed to create preview of apropriate size! "
-                            + " in: %dx%d, out: %dx%d",
-                            mWidth, mHeight,
-                            preview.getWidth(), preview.getHeight()));
-                }
+            } else {
+                Log.w(TAG, String.format(
+                        "Failed to create preview of apropriate size! "
+                        + " in: %dx%d, out: %dx%d",
+                        mWidth, mHeight,
+                        preview.getWidth(), preview.getHeight()));
             }
         }
     }
 
+    public Bitmap getBitmap() {
+        return mPreview instanceof BitmapTexture ? ((BitmapTexture) mPreview).getBitmap() : null;
+    }
+
     @Override
     public int getTileSize() {
         return mTileSize;
@@ -435,10 +446,6 @@
     @Override
     public Bitmap getTile(int level, int x, int y, Bitmap bitmap) {
         int tileSize = getTileSize();
-        if (!REUSE_BITMAP) {
-            return getTileWithoutReusingBitmap(level, x, y, tileSize);
-        }
-
         int t = tileSize << level;
         mWantRegion.set(x, y, x + t, y + t);
 
@@ -462,64 +469,4 @@
         }
         return bitmap;
     }
-
-    private Bitmap getTileWithoutReusingBitmap(
-            int level, int x, int y, int tileSize) {
-
-        int t = tileSize << level;
-        mWantRegion.set(x, y, x + t, y + t);
-
-        mOverlapRegion.set(0, 0, mWidth, mHeight);
-
-        mOptions.inSampleSize = (1 << level);
-        Bitmap bitmap = mDecoder.decodeRegion(mOverlapRegion, mOptions);
-
-        if (bitmap == null) {
-            Log.w(TAG, "fail in decoding region");
-        }
-
-        if (mWantRegion.equals(mOverlapRegion)) {
-            return bitmap;
-        }
-
-        Bitmap result = Bitmap.createBitmap(tileSize, tileSize, Config.ARGB_8888);
-        if (mCanvas == null) {
-            mCanvas = new Canvas();
-        }
-        mCanvas.setBitmap(result);
-        mCanvas.drawBitmap(bitmap,
-                (mOverlapRegion.left - mWantRegion.left) >> level,
-                (mOverlapRegion.top - mWantRegion.top) >> level, null);
-        mCanvas.setBitmap(null);
-        return result;
-    }
-
-    /**
-     * Note that the returned bitmap may have a long edge that's longer
-     * than the targetSize, but it will always be less than 2x the targetSize
-     */
-    private Bitmap decodePreview(BitmapSource source, int targetSize) {
-        Bitmap result = source.getPreviewBitmap();
-        if (result == null) {
-            return null;
-        }
-
-        // We need to resize down if the decoder does not support inSampleSize
-        // or didn't support the specified inSampleSize (some decoders only do powers of 2)
-        float scale = (float) targetSize / (float) (Math.max(result.getWidth(), result.getHeight()));
-
-        if (scale <= 0.5) {
-            result = BitmapUtils.resizeBitmapByScale(result, scale, true);
-        }
-        return ensureGLCompatibleBitmap(result);
-    }
-
-    private static Bitmap ensureGLCompatibleBitmap(Bitmap bitmap) {
-        if (bitmap == null || bitmap.getConfig() != null) {
-            return bitmap;
-        }
-        Bitmap newBitmap = bitmap.copy(Config.ARGB_8888, false);
-        bitmap.recycle();
-        return newBitmap;
-    }
 }
diff --git a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java b/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java
deleted file mode 100644
index 8a05051..0000000
--- a/WallpaperPicker/src/com/android/photos/views/BlockingGLTextureView.java
+++ /dev/null
@@ -1,438 +0,0 @@
-/*
- * 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.photos.views;
-
-import android.content.Context;
-import android.graphics.SurfaceTexture;
-import android.opengl.GLSurfaceView.Renderer;
-import android.opengl.GLUtils;
-import android.util.Log;
-import android.view.TextureView;
-import android.view.TextureView.SurfaceTextureListener;
-
-import javax.microedition.khronos.egl.EGL10;
-import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.egl.EGLContext;
-import javax.microedition.khronos.egl.EGLDisplay;
-import javax.microedition.khronos.egl.EGLSurface;
-import javax.microedition.khronos.opengles.GL10;
-
-/**
- * A TextureView that supports blocking rendering for synchronous drawing
- */
-public class BlockingGLTextureView extends TextureView
-        implements SurfaceTextureListener {
-
-    private RenderThread mRenderThread;
-
-    public BlockingGLTextureView(Context context) {
-        super(context);
-        setSurfaceTextureListener(this);
-    }
-
-    public void setRenderer(Renderer renderer) {
-        if (mRenderThread != null) {
-            throw new IllegalArgumentException("Renderer already set");
-        }
-        mRenderThread = new RenderThread(renderer);
-    }
-
-    public void render() {
-        mRenderThread.render();
-    }
-
-    public void destroy() {
-        if (mRenderThread != null) {
-            mRenderThread.finish();
-            mRenderThread = null;
-        }
-    }
-
-    @Override
-    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
-            int height) {
-        mRenderThread.setSurface(surface);
-        mRenderThread.setSize(width, height);
-    }
-
-    @Override
-    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
-            int height) {
-        mRenderThread.setSize(width, height);
-    }
-
-    @Override
-    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
-        if (mRenderThread != null) {
-            mRenderThread.setSurface(null);
-        }
-        return false;
-    }
-
-    @Override
-    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
-    }
-
-    @Override
-    protected void finalize() throws Throwable {
-        try {
-            destroy();
-        } catch (Throwable t) {
-            // Ignore
-        }
-        super.finalize();
-    }
-
-    /**
-     * An EGL helper class.
-     */
-
-    private static class EglHelper {
-        private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
-        private static final int EGL_OPENGL_ES2_BIT = 4;
-
-        EGL10 mEgl;
-        EGLDisplay mEglDisplay;
-        EGLSurface mEglSurface;
-        EGLConfig mEglConfig;
-        EGLContext mEglContext;
-
-        private EGLConfig chooseEglConfig() {
-            int[] configsCount = new int[1];
-            EGLConfig[] configs = new EGLConfig[1];
-            int[] configSpec = getConfig();
-            if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
-                throw new IllegalArgumentException("eglChooseConfig failed " +
-                        GLUtils.getEGLErrorString(mEgl.eglGetError()));
-            } else if (configsCount[0] > 0) {
-                return configs[0];
-            }
-            return null;
-        }
-
-        private static int[] getConfig() {
-            return new int[] {
-                    EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
-                    EGL10.EGL_RED_SIZE, 8,
-                    EGL10.EGL_GREEN_SIZE, 8,
-                    EGL10.EGL_BLUE_SIZE, 8,
-                    EGL10.EGL_ALPHA_SIZE, 8,
-                    EGL10.EGL_DEPTH_SIZE, 0,
-                    EGL10.EGL_STENCIL_SIZE, 0,
-                    EGL10.EGL_NONE
-            };
-        }
-
-        EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
-            int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
-            return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
-        }
-
-        /**
-         * Initialize EGL for a given configuration spec.
-         */
-        public void start() {
-            /*
-             * Get an EGL instance
-             */
-            mEgl = (EGL10) EGLContext.getEGL();
-
-            /*
-             * Get to the default display.
-             */
-            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
-
-            if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
-                throw new RuntimeException("eglGetDisplay failed");
-            }
-
-            /*
-             * We can now initialize EGL for that display
-             */
-            int[] version = new int[2];
-            if (!mEgl.eglInitialize(mEglDisplay, version)) {
-                throw new RuntimeException("eglInitialize failed");
-            }
-            mEglConfig = chooseEglConfig();
-
-            /*
-            * Create an EGL context. We want to do this as rarely as we can, because an
-            * EGL context is a somewhat heavy object.
-            */
-            mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
-
-            if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
-                mEglContext = null;
-                throwEglException("createContext");
-            }
-
-            mEglSurface = null;
-        }
-
-        /**
-         * Create an egl surface for the current SurfaceTexture surface. If a surface
-         * already exists, destroy it before creating the new surface.
-         *
-         * @return true if the surface was created successfully.
-         */
-        public boolean createSurface(SurfaceTexture surface) {
-            /*
-             * Check preconditions.
-             */
-            if (mEgl == null) {
-                throw new RuntimeException("egl not initialized");
-            }
-            if (mEglDisplay == null) {
-                throw new RuntimeException("eglDisplay not initialized");
-            }
-            if (mEglConfig == null) {
-                throw new RuntimeException("mEglConfig not initialized");
-            }
-
-            /*
-             *  The window size has changed, so we need to create a new
-             *  surface.
-             */
-            destroySurfaceImp();
-
-            /*
-             * Create an EGL surface we can render into.
-             */
-            if (surface != null) {
-                mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null);
-            } else {
-                mEglSurface = null;
-            }
-
-            if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
-                int error = mEgl.eglGetError();
-                if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
-                    Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
-                }
-                return false;
-            }
-
-            /*
-             * Before we can issue GL commands, we need to make sure
-             * the context is current and bound to a surface.
-             */
-            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
-                /*
-                 * Could not make the context current, probably because the underlying
-                 * SurfaceView surface has been destroyed.
-                 */
-                logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
-                return false;
-            }
-
-            return true;
-        }
-
-        /**
-         * Create a GL object for the current EGL context.
-         */
-        public GL10 createGL() {
-            return (GL10) mEglContext.getGL();
-        }
-
-        /**
-         * Display the current render surface.
-         * @return the EGL error code from eglSwapBuffers.
-         */
-        public int swap() {
-            if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
-                return mEgl.eglGetError();
-            }
-            return EGL10.EGL_SUCCESS;
-        }
-
-        public void destroySurface() {
-            destroySurfaceImp();
-        }
-
-        private void destroySurfaceImp() {
-            if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
-                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
-                        EGL10.EGL_NO_SURFACE,
-                        EGL10.EGL_NO_CONTEXT);
-                mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
-                mEglSurface = null;
-            }
-        }
-
-        public void finish() {
-            if (mEglContext != null) {
-                mEgl.eglDestroyContext(mEglDisplay, mEglContext);
-                mEglContext = null;
-            }
-            if (mEglDisplay != null) {
-                mEgl.eglTerminate(mEglDisplay);
-                mEglDisplay = null;
-            }
-        }
-
-        private void throwEglException(String function) {
-            throwEglException(function, mEgl.eglGetError());
-        }
-
-        public static void throwEglException(String function, int error) {
-            String message = formatEglError(function, error);
-            throw new RuntimeException(message);
-        }
-
-        public static void logEglErrorAsWarning(String tag, String function, int error) {
-            Log.w(tag, formatEglError(function, error));
-        }
-
-        public static String formatEglError(String function, int error) {
-            return function + " failed: " + error;
-        }
-
-    }
-
-    private static class RenderThread extends Thread {
-        private static final int INVALID = -1;
-        private static final int RENDER = 1;
-        private static final int CHANGE_SURFACE = 2;
-        private static final int RESIZE_SURFACE = 3;
-        private static final int FINISH = 4;
-
-        private EglHelper mEglHelper = new EglHelper();
-
-        private Object mLock = new Object();
-        private int mExecMsgId = INVALID;
-        private SurfaceTexture mSurface;
-        private Renderer mRenderer;
-        private int mWidth, mHeight;
-
-        private boolean mFinished = false;
-        private GL10 mGL;
-
-        public RenderThread(Renderer renderer) {
-            super("RenderThread");
-            mRenderer = renderer;
-            start();
-        }
-
-        private void checkRenderer() {
-            if (mRenderer == null) {
-                throw new IllegalArgumentException("Renderer is null!");
-            }
-        }
-
-        private void checkSurface() {
-            if (mSurface == null) {
-                throw new IllegalArgumentException("surface is null!");
-            }
-        }
-
-        public void setSurface(SurfaceTexture surface) {
-            // If the surface is null we're being torn down, don't need a
-            // renderer then
-            if (surface != null) {
-                checkRenderer();
-            }
-            mSurface = surface;
-            exec(CHANGE_SURFACE);
-        }
-
-        public void setSize(int width, int height) {
-            checkRenderer();
-            checkSurface();
-            mWidth = width;
-            mHeight = height;
-            exec(RESIZE_SURFACE);
-        }
-
-        public void render() {
-            checkRenderer();
-            if (mSurface != null) {
-                exec(RENDER);
-                mSurface.updateTexImage();
-            }
-        }
-
-        public void finish() {
-            mSurface = null;
-            exec(FINISH);
-            try {
-                join();
-            } catch (InterruptedException e) {
-                // Ignore
-            }
-        }
-
-        private void exec(int msgid) {
-            synchronized (mLock) {
-                if (mExecMsgId != INVALID) {
-                    throw new IllegalArgumentException(
-                            "Message already set - multithreaded access?");
-                }
-                mExecMsgId = msgid;
-                mLock.notify();
-                try {
-                    mLock.wait();
-                } catch (InterruptedException e) {
-                    // Ignore
-                }
-            }
-        }
-
-        private void handleMessageLocked(int what) {
-            switch (what) {
-            case CHANGE_SURFACE:
-                if (mEglHelper.createSurface(mSurface)) {
-                    mGL = mEglHelper.createGL();
-                    mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig);
-                }
-                break;
-            case RESIZE_SURFACE:
-                mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
-                break;
-            case RENDER:
-                mRenderer.onDrawFrame(mGL);
-                mEglHelper.swap();
-                break;
-            case FINISH:
-                mEglHelper.destroySurface();
-                mEglHelper.finish();
-                mFinished = true;
-                break;
-            }
-        }
-
-        @Override
-        public void run() {
-            synchronized (mLock) {
-                mEglHelper.start();
-                while (!mFinished) {
-                    while (mExecMsgId == INVALID) {
-                        try {
-                            mLock.wait();
-                        } catch (InterruptedException e) {
-                            // Ignore
-                        }
-                    }
-                    handleMessageLocked(mExecMsgId);
-                    mExecMsgId = INVALID;
-                    mLock.notify();
-                }
-                mExecMsgId = FINISH;
-            }
-        }
-    }
-}
diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java
index c4e493b..e57ce70 100644
--- a/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java
+++ b/WallpaperPicker/src/com/android/photos/views/TiledImageRenderer.java
@@ -20,11 +20,11 @@
 import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.support.v4.util.LongSparseArray;
+import android.support.v4.util.Pools.Pool;
+import android.support.v4.util.Pools.SynchronizedPool;
 import android.util.DisplayMetrics;
 import android.util.Log;
-import android.util.Pools.Pool;
-import android.util.Pools.SynchronizedPool;
+import android.util.LongSparseArray;
 import android.view.View;
 import android.view.WindowManager;
 
@@ -32,6 +32,7 @@
 import com.android.gallery3d.glrenderer.BasicTexture;
 import com.android.gallery3d.glrenderer.GLCanvas;
 import com.android.gallery3d.glrenderer.UploadedTexture;
+import com.android.launcher3.util.Thunk;
 
 /**
  * Handles laying out, decoding, and drawing of tiles in GL
@@ -67,12 +68,12 @@
     private static final int STATE_RECYCLING = 0x20;
     private static final int STATE_RECYCLED = 0x40;
 
-    private static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
+    @Thunk static Pool<Bitmap> sTilePool = new SynchronizedPool<Bitmap>(64);
 
     // TILE_SIZE must be 2^N
-    private int mTileSize;
+    @Thunk int mTileSize;
 
-    private TileSource mModel;
+    @Thunk TileSource mModel;
     private BasicTexture mPreview;
     protected int mLevelCount;  // cache the value of mScaledBitmaps.length
 
@@ -82,7 +83,7 @@
     // half size of the previous one). If the value is in [0, mLevelCount), we
     // use the bitmap in mScaledBitmaps[mLevel] for display, otherwise the value
     // is mLevelCount
-    private int mLevel = 0;
+    @Thunk int mLevel = 0;
 
     private int mOffsetX;
     private int mOffsetY;
@@ -96,10 +97,10 @@
     private final LongSparseArray<Tile> mActiveTiles = new LongSparseArray<Tile>();
 
     // The following three queue are guarded by mQueueLock
-    private final Object mQueueLock = new Object();
+    @Thunk final Object mQueueLock = new Object();
     private final TileQueue mRecycledQueue = new TileQueue();
     private final TileQueue mUploadQueue = new TileQueue();
-    private final TileQueue mDecodeQueue = new TileQueue();
+    @Thunk final TileQueue mDecodeQueue = new TileQueue();
 
     // The width and height of the full-sized bitmap
     protected int mImageWidth = SIZE_UNKNOWN;
@@ -489,7 +490,7 @@
        }
     }
 
-    private void decodeTile(Tile tile) {
+    @Thunk void decodeTile(Tile tile) {
         synchronized (mQueueLock) {
             if (tile.mTileState != STATE_IN_QUEUE) {
                 return;
@@ -556,7 +557,7 @@
         mActiveTiles.put(key, tile);
     }
 
-    private Tile getTile(int x, int y, int level) {
+    @Thunk Tile getTile(int x, int y, int level) {
         return mActiveTiles.get(makeTileKey(x, y, level));
     }
 
@@ -748,7 +749,7 @@
         }
     }
 
-    private static class TileQueue {
+    @Thunk static class TileQueue {
         private Tile mHead;
 
         public Tile pop() {
@@ -786,7 +787,7 @@
         }
     }
 
-    private class TileDecoder extends Thread {
+    @Thunk class TileDecoder extends Thread {
 
         public void finishAndWait() {
             interrupt();
diff --git a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java
index 94063b0..7e3e1a9 100644
--- a/WallpaperPicker/src/com/android/photos/views/TiledImageView.java
+++ b/WallpaperPicker/src/com/android/photos/views/TiledImageView.java
@@ -16,8 +16,6 @@
 
 package com.android.photos.views;
 
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -28,35 +26,26 @@
 import android.graphics.RectF;
 import android.opengl.GLSurfaceView;
 import android.opengl.GLSurfaceView.Renderer;
-import android.os.Build;
 import android.util.AttributeSet;
 import android.view.Choreographer;
 import android.view.Choreographer.FrameCallback;
-import android.view.View;
 import android.widget.FrameLayout;
 
 import com.android.gallery3d.glrenderer.BasicTexture;
 import com.android.gallery3d.glrenderer.GLES20Canvas;
+import com.android.launcher3.util.Thunk;
 import com.android.photos.views.TiledImageRenderer.TileSource;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 
 /**
- * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}
- * or {@link BlockingGLTextureView}.
+ * Shows an image using {@link TiledImageRenderer} using either {@link GLSurfaceView}.
  */
 public class TiledImageView extends FrameLayout {
 
-    private static final boolean USE_TEXTURE_VIEW = false;
-    private static final boolean IS_SUPPORTED =
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
-    private static final boolean USE_CHOREOGRAPHER =
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
-
-    private BlockingGLTextureView mTextureView;
-    private GLSurfaceView mGLSurfaceView;
-    private boolean mInvalPending = false;
+    @Thunk GLSurfaceView mGLSurfaceView;
+    @Thunk boolean mInvalPending = false;
     private FrameCallback mFrameCallback;
 
     protected static class ImageRendererWrapper {
@@ -79,35 +68,19 @@
     protected Object mLock = new Object();
     protected ImageRendererWrapper mRenderer;
 
-    public static boolean isTilingSupported() {
-        return IS_SUPPORTED;
-    }
-
     public TiledImageView(Context context) {
         this(context, null);
     }
 
     public TiledImageView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        if (!IS_SUPPORTED) {
-            return;
-        }
-
         mRenderer = new ImageRendererWrapper();
         mRenderer.image = new TiledImageRenderer(this);
-        View view;
-        if (USE_TEXTURE_VIEW) {
-            mTextureView = new BlockingGLTextureView(context);
-            mTextureView.setRenderer(new TileRenderer());
-            view = mTextureView;
-        } else {
-            mGLSurfaceView = new GLSurfaceView(context);
-            mGLSurfaceView.setEGLContextClientVersion(2);
-            mGLSurfaceView.setRenderer(new TileRenderer());
-            mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
-            view = mGLSurfaceView;
-        }
-        addView(view, new LayoutParams(
+        mGLSurfaceView = new GLSurfaceView(context);
+        mGLSurfaceView.setEGLContextClientVersion(2);
+        mGLSurfaceView.setRenderer(new TileRenderer());
+        mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
+        addView(mGLSurfaceView, new LayoutParams(
                 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
         //setTileSource(new ColoredTiles());
     }
@@ -117,22 +90,11 @@
         super.setVisibility(visibility);
         // need to update inner view's visibility because it seems like we're causing it to draw
         // from {@link #dispatchDraw} or {@link #invalidate} even if we are invisible.
-        if (USE_TEXTURE_VIEW) {
-            mTextureView.setVisibility(visibility);
-        } else {
-            mGLSurfaceView.setVisibility(visibility);
-        }
+        mGLSurfaceView.setVisibility(visibility);
     }
 
     public void destroy() {
-        if (!IS_SUPPORTED) {
-            return;
-        }
-        if (USE_TEXTURE_VIEW) {
-            mTextureView.destroy();
-        } else {
-            mGLSurfaceView.queueEvent(mFreeTextures);
-        }
+        mGLSurfaceView.queueEvent(mFreeTextures);
     }
 
     private Runnable mFreeTextures = new Runnable() {
@@ -144,27 +106,14 @@
     };
 
     public void onPause() {
-        if (!IS_SUPPORTED) {
-            return;
-        }
-        if (!USE_TEXTURE_VIEW) {
-            mGLSurfaceView.onPause();
-        }
+        mGLSurfaceView.onPause();
     }
 
     public void onResume() {
-        if (!IS_SUPPORTED) {
-            return;
-        }
-        if (!USE_TEXTURE_VIEW) {
-            mGLSurfaceView.onResume();
-        }
+        mGLSurfaceView.onResume();
     }
 
     public void setTileSource(TileSource source, Runnable isReadyCallback) {
-        if (!IS_SUPPORTED) {
-            return;
-        }
         synchronized (mLock) {
             mRenderer.source = source;
             mRenderer.isReadyCallback = isReadyCallback;
@@ -177,13 +126,14 @@
         invalidate();
     }
 
+    public TileSource getTileSource() {
+        return mRenderer.source;
+    }
+
     @Override
     protected void onLayout(boolean changed, int left, int top, int right,
             int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        if (!IS_SUPPORTED) {
-            return;
-        }
         synchronized (mLock) {
             updateScaleIfNecessaryLocked(mRenderer);
         }
@@ -200,43 +150,10 @@
     }
 
     @Override
-    protected void dispatchDraw(Canvas canvas) {
-        if (!IS_SUPPORTED) {
-            return;
-        }
-        if (USE_TEXTURE_VIEW) {
-            mTextureView.render();
-        }
-        super.dispatchDraw(canvas);
-    }
-
-    @SuppressLint("NewApi")
-    @Override
-    public void setTranslationX(float translationX) {
-        if (!IS_SUPPORTED) {
-            return;
-        }
-        super.setTranslationX(translationX);
-    }
-
-    @Override
     public void invalidate() {
-        if (!IS_SUPPORTED) {
-            return;
-        }
-        if (USE_TEXTURE_VIEW) {
-            super.invalidate();
-            mTextureView.invalidate();
-        } else {
-            if (USE_CHOREOGRAPHER) {
-                invalOnVsync();
-            } else {
-                mGLSurfaceView.requestRender();
-            }
-        }
+        invalOnVsync();
     }
 
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     private void invalOnVsync() {
         if (!mInvalPending) {
             mInvalPending = true;
@@ -255,9 +172,6 @@
 
     private RectF mTempRectF = new RectF();
     public void positionFromMatrix(Matrix matrix) {
-        if (!IS_SUPPORTED) {
-            return;
-        }
         if (mRenderer.source != null) {
             final int rotation = mRenderer.source.getRotation();
             final boolean swap = !(rotation % 180 == 0);
@@ -290,7 +204,7 @@
         }
     }
 
-    private class TileRenderer implements Renderer {
+    @Thunk class TileRenderer implements Renderer {
 
         private GLES20Canvas mCanvas;
 
diff --git a/proguard.flags b/proguard.flags
index 83a491d..e2a4b5b 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -6,7 +6,6 @@
   public void onClickVoiceButton(android.view.View);
   public void onClickConfigureButton(android.view.View);
   public void onClickAllAppsButton(android.view.View);
-  public void onClickAppMarketButton(android.view.View);
   public void dismissFirstRunCling(android.view.View);
   public void dismissMigrationClingCopyApps(android.view.View);
   public void dismissMigrationClingUseDefault(android.view.View);
@@ -62,3 +61,8 @@
   public int getBrightness();
   public void setBrightness(int);
 }
+
+-keep class com.android.launcher3.AppsContainerRecyclerView {
+  public void setFastScrollerAlpha(float);
+  public float getFastScrollerAlpha();
+}
\ No newline at end of file
diff --git a/protos/backup.proto b/protos/backup.proto
index 44c4b09..09330ee 100644
--- a/protos/backup.proto
+++ b/protos/backup.proto
@@ -72,6 +72,17 @@
 }
 
 message Favorite {
+  // Type of the app, this target represents
+  enum TargetType {
+    TARGET_NONE = 0;
+    TARGET_PHONE = 1;
+    TARGET_MESSENGER = 2;
+    TARGET_EMAIL = 3;
+    TARGET_BROWSER = 4;
+    TARGET_GALLERY = 5;
+    TARGET_CAMERA = 6;
+  }
+
   required int64 id = 1;
   required int32 itemType = 2;
   optional string title = 3;
@@ -90,6 +101,7 @@
   optional string iconPackage = 16;
   optional string iconResource = 17;
   optional bytes icon = 18;
+  optional TargetType targetType = 19 [default = TARGET_NONE];
  }
 
 message Screen {
diff --git a/res/drawable-hdpi/ic_launcher_market_holo.png b/res/drawable-hdpi/ic_launcher_market_holo.png
deleted file mode 100644
index dc78251..0000000
--- a/res/drawable-hdpi/ic_launcher_market_holo.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-hdpi/ic_pageindicator_current.png b/res/drawable-hdpi/ic_pageindicator_current.png
index 283f44d..423ca2b 100644
--- a/res/drawable-hdpi/ic_pageindicator_current.png
+++ b/res/drawable-hdpi/ic_pageindicator_current.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pageindicator_current_folder.png b/res/drawable-hdpi/ic_pageindicator_current_folder.png
new file mode 100644
index 0000000..43fbb0e
--- /dev/null
+++ b/res/drawable-hdpi/ic_pageindicator_current_folder.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pageindicator_default.png b/res/drawable-hdpi/ic_pageindicator_default.png
index 47b9989..83fa73f 100644
--- a/res/drawable-hdpi/ic_pageindicator_default.png
+++ b/res/drawable-hdpi/ic_pageindicator_default.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_pageindicator_default_folder.png b/res/drawable-hdpi/ic_pageindicator_default_folder.png
new file mode 100644
index 0000000..55cab1c
--- /dev/null
+++ b/res/drawable-hdpi/ic_pageindicator_default_folder.png
Binary files differ
diff --git a/res/drawable-ldrtl/apps_list_fastscroll_bg.xml b/res/drawable-ldrtl/apps_list_fastscroll_bg.xml
new file mode 100644
index 0000000..772975a
--- /dev/null
+++ b/res/drawable-ldrtl/apps_list_fastscroll_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/apps_view_scrollbar_thumb_color" />
+    <size
+        android:width="64dp"
+        android:height="64dp" />
+    <corners
+        android:topLeftRadius="64dp"
+        android:topRightRadius="64dp"
+        android:bottomRightRadius="64dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable-mdpi/ic_launcher_market_holo.png b/res/drawable-mdpi/ic_launcher_market_holo.png
deleted file mode 100644
index cacb374..0000000
--- a/res/drawable-mdpi/ic_launcher_market_holo.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/ic_pageindicator_current.png b/res/drawable-mdpi/ic_pageindicator_current.png
index b41e1bb..ca889c4 100644
--- a/res/drawable-mdpi/ic_pageindicator_current.png
+++ b/res/drawable-mdpi/ic_pageindicator_current.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pageindicator_current_folder.png b/res/drawable-mdpi/ic_pageindicator_current_folder.png
new file mode 100644
index 0000000..5bbba91
--- /dev/null
+++ b/res/drawable-mdpi/ic_pageindicator_current_folder.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pageindicator_default.png b/res/drawable-mdpi/ic_pageindicator_default.png
index e36c25c..34493b1 100644
--- a/res/drawable-mdpi/ic_pageindicator_default.png
+++ b/res/drawable-mdpi/ic_pageindicator_default.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_pageindicator_default_folder.png b/res/drawable-mdpi/ic_pageindicator_default_folder.png
new file mode 100644
index 0000000..0a987a4
--- /dev/null
+++ b/res/drawable-mdpi/ic_pageindicator_default_folder.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_launcher_market_holo.png b/res/drawable-xhdpi/ic_launcher_market_holo.png
deleted file mode 100644
index 958f0de..0000000
--- a/res/drawable-xhdpi/ic_launcher_market_holo.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pageindicator_current.png b/res/drawable-xhdpi/ic_pageindicator_current.png
index 8fa774d..3054f2f 100644
--- a/res/drawable-xhdpi/ic_pageindicator_current.png
+++ b/res/drawable-xhdpi/ic_pageindicator_current.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pageindicator_current_folder.png b/res/drawable-xhdpi/ic_pageindicator_current_folder.png
new file mode 100644
index 0000000..cd92e9f
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pageindicator_current_folder.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pageindicator_default.png b/res/drawable-xhdpi/ic_pageindicator_default.png
index 8eb5eb0..38538dc 100644
--- a/res/drawable-xhdpi/ic_pageindicator_default.png
+++ b/res/drawable-xhdpi/ic_pageindicator_default.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_pageindicator_default_folder.png b/res/drawable-xhdpi/ic_pageindicator_default_folder.png
new file mode 100644
index 0000000..e7c46e3
--- /dev/null
+++ b/res/drawable-xhdpi/ic_pageindicator_default_folder.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_pageindicator_current.png b/res/drawable-xxhdpi/ic_pageindicator_current.png
index 22b290e..5941c8e 100644
--- a/res/drawable-xxhdpi/ic_pageindicator_current.png
+++ b/res/drawable-xxhdpi/ic_pageindicator_current.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_pageindicator_current_folder.png b/res/drawable-xxhdpi/ic_pageindicator_current_folder.png
new file mode 100644
index 0000000..602b89a
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_pageindicator_current_folder.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_pageindicator_default.png b/res/drawable-xxhdpi/ic_pageindicator_default.png
index e608cae..3fa9e5f 100644
--- a/res/drawable-xxhdpi/ic_pageindicator_default.png
+++ b/res/drawable-xxhdpi/ic_pageindicator_default.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_pageindicator_default_folder.png b/res/drawable-xxhdpi/ic_pageindicator_default_folder.png
new file mode 100644
index 0000000..bbcd7f9
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_pageindicator_default_folder.png
Binary files differ
diff --git a/res/drawable/apps_list_bg.xml b/res/drawable/apps_list_bg.xml
new file mode 100644
index 0000000..0e56684
--- /dev/null
+++ b/res/drawable/apps_list_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#ffffff" />
+    <corners
+        android:radius="2dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_list_fastscroll_bg.xml b/res/drawable/apps_list_fastscroll_bg.xml
new file mode 100644
index 0000000..780d3b0
--- /dev/null
+++ b/res/drawable/apps_list_fastscroll_bg.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/apps_view_scrollbar_thumb_color" />
+    <size
+        android:width="64dp"
+        android:height="64dp" />
+    <corners
+        android:topLeftRadius="64dp"
+        android:topRightRadius="64dp"
+        android:bottomLeftRadius="64dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_list_scrollbar_thumb.xml b/res/drawable/apps_list_scrollbar_thumb.xml
new file mode 100644
index 0000000..318d406
--- /dev/null
+++ b/res/drawable/apps_list_scrollbar_thumb.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="@color/apps_view_scrollbar_thumb_color" />
+    <size android:width="@dimen/apps_view_fast_scroll_bar_width" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_list_search_bg.xml b/res/drawable/apps_list_search_bg.xml
new file mode 100644
index 0000000..63c4d55
--- /dev/null
+++ b/res/drawable/apps_list_search_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#ffffff" />
+    <corners
+        android:bottomLeftRadius="2dp"
+        android:bottomRightRadius="2dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_reveal_bg.xml b/res/drawable/apps_reveal_bg.xml
new file mode 100644
index 0000000..07505a5
--- /dev/null
+++ b/res/drawable/apps_reveal_bg.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#ffffff" />
+    <corners android:radius="2dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/apps_search_bg.xml b/res/drawable/apps_search_bg.xml
new file mode 100644
index 0000000..405e844
--- /dev/null
+++ b/res/drawable/apps_search_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <solid android:color="#ffffff" />
+    <corners
+        android:topLeftRadius="2dp"
+        android:topRightRadius="2dp" />
+</shape>
\ No newline at end of file
diff --git a/res/layout-land/launcher.xml b/res/layout-land/launcher.xml
index 6f95bd5..d5dd91a 100644
--- a/res/layout-land/launcher.xml
+++ b/res/layout-land/launcher.xml
@@ -57,8 +57,14 @@
             android:id="@+id/overview_panel"
             android:visibility="gone" />
 
-        <include layout="@layout/apps_customize_pane"
-            android:id="@+id/apps_customize_pane"
+        <include layout="@layout/widgets_view"
+            android:id="@+id/widgets_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="invisible" />
+
+        <include layout="@layout/apps_view"
+            android:id="@+id/apps_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:visibility="invisible" />
diff --git a/res/layout-port/launcher.xml b/res/layout-port/launcher.xml
index af30a32..5a018c5 100644
--- a/res/layout-port/launcher.xml
+++ b/res/layout-port/launcher.xml
@@ -66,8 +66,14 @@
             android:id="@+id/search_drop_target_bar"
             layout="@layout/search_drop_target_bar" />
 
-        <include layout="@layout/apps_customize_pane"
-            android:id="@+id/apps_customize_pane"
+        <include layout="@layout/widgets_view"
+            android:id="@+id/widgets_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="invisible" />
+
+        <include layout="@layout/apps_view"
+            android:id="@+id/apps_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:visibility="invisible" />
diff --git a/res/layout-sw600dp/apps_view.xml b/res/layout-sw600dp/apps_view.xml
new file mode 100644
index 0000000..e6e0ec3
--- /dev/null
+++ b/res/layout-sw600dp/apps_view.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.launcher3.AppsContainerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/apps_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="@dimen/apps_container_inset"
+    android:descendantFocusability="afterDescendants">
+    <include
+        layout="@layout/apps_reveal_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+    <include
+        layout="@layout/apps_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+</com.android.launcher3.AppsContainerView>
\ No newline at end of file
diff --git a/res/layout-sw720dp/launcher.xml b/res/layout-sw720dp/launcher.xml
index 960ccf3..a9601af 100644
--- a/res/layout-sw720dp/launcher.xml
+++ b/res/layout-sw720dp/launcher.xml
@@ -66,8 +66,14 @@
             android:layout_height="wrap_content"
             android:layout_gravity="center_horizontal" />
 
-        <include layout="@layout/apps_customize_pane"
-            android:id="@+id/apps_customize_pane"
+        <include layout="@layout/widgets_view"
+            android:id="@+id/widgets_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="invisible" />
+
+        <include layout="@layout/apps_view"
+            android:id="@+id/apps_view"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:visibility="invisible" />
diff --git a/res/layout/apps_customize_pane.xml b/res/layout/apps_customize_pane.xml
deleted file mode 100644
index e42576f..0000000
--- a/res/layout/apps_customize_pane.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 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.
--->
-<com.android.launcher3.AppsCustomizeTabHost
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:clipChildren="false">
-
-    <LinearLayout
-        android:id="@+id/content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clipChildren="false"
-        android:orientation="vertical">
-
-        <FrameLayout
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1"
-            android:clipChildren="false">
-            <FrameLayout
-                android:id="@+id/fake_page_container"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                android:clipChildren="false"
-                android:clipToPadding="false">
-                <FrameLayout
-                    android:id="@+id/fake_page"
-                    android:layout_width="match_parent"
-                    android:layout_height="match_parent"
-                    android:visibility="invisible"
-                    android:clipToPadding="false" />
-            </FrameLayout>
-            <com.android.launcher3.AppsCustomizePagedView
-                android:id="@+id/apps_customize_pane_content"
-                android:layout_width="match_parent"
-                android:layout_height="match_parent"
-                launcher:widgetCountX="@integer/apps_customize_widget_cell_count_x"
-                launcher:widgetCountY="@integer/apps_customize_widget_cell_count_y"
-                launcher:maxGap="@dimen/workspace_max_gap"
-                launcher:pageIndicator="@+id/apps_customize_page_indicator" />
-        </FrameLayout>
-        <include
-            android:id="@+id/apps_customize_page_indicator"
-            layout="@layout/page_indicator"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center" />
-    </LinearLayout>
-</com.android.launcher3.AppsCustomizeTabHost>
diff --git a/res/layout/apps_customize_widget.xml b/res/layout/apps_customize_widget.xml
deleted file mode 100644
index a8344e3..0000000
--- a/res/layout/apps_customize_widget.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 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.
--->
-<com.android.launcher3.PagedViewWidget
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:launcher="http://schemas.android.com/apk/res-auto"
-
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:layout_weight="1"
-    android:orientation="vertical"
-
-    android:background="@drawable/focusable_view_bg"
-    android:focusable="true">
-
-    <LinearLayout
-        android:orientation="horizontal"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_weight="1">
-        <FrameLayout
-            android:id="@+id/left_border"
-            android:layout_width="1dp"
-            android:layout_height="match_parent"
-            android:background="@color/widget_text_panel"
-            android:visibility="gone" />
-
-        <!-- The preview of the widget or shortcut. -->
-        <com.android.launcher3.PagedViewWidgetImageView
-            android:id="@+id/widget_preview"
-            style="@style/PagedViewWidgetImageView"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:paddingTop="@dimen/app_widget_preview_padding_top"
-            android:paddingEnd="@dimen/app_widget_preview_padding_right"
-            android:paddingRight="@dimen/app_widget_preview_padding_right"
-            android:scaleType="matrix" />
-        <FrameLayout
-            android:id="@+id/right_border"
-            android:layout_width="1dp"
-            android:layout_height="match_parent"
-            android:background="@color/widget_text_panel"
-            android:visibility="gone" />
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:paddingTop="@dimen/app_widget_preview_label_vertical_padding"
-        android:paddingBottom="@dimen/app_widget_preview_label_vertical_padding"
-        android:paddingLeft="@dimen/app_widget_preview_label_horizontal_padding"
-        android:paddingRight="@dimen/app_widget_preview_label_horizontal_padding"
-        android:background="@color/widget_text_panel"
-        android:orientation="horizontal">
-        <!-- The name of the widget. -->
-        <TextView
-            android:id="@+id/widget_name"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:gravity="start"
-            android:singleLine="true"
-            android:ellipsize="marquee"
-            android:fadingEdge="horizontal"
-
-            android:textColor="#FFFFFFFF"
-            android:textSize="12sp"
-            android:textAlignment="viewStart"
-            android:fontFamily="sans-serif-condensed"
-            android:shadowRadius="2.0"
-            android:shadowColor="#B0000000" />
-
-        <!-- The original dimensions of the widget (can't be the same text as above due to different
-             style. -->
-        <TextView
-            android:id="@+id/widget_dims"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:layout_marginStart="5dp"
-            android:layout_weight="0"
-            android:gravity="start"
-
-            android:textColor="#FFFFFFFF"
-            android:textSize="12sp"
-            android:fontFamily="sans-serif-condensed"
-            android:shadowRadius="2.0"
-            android:shadowColor="#B0000000" />
-    </LinearLayout>
-
-
-</com.android.launcher3.PagedViewWidget>
diff --git a/res/layout/apps_empty_view.xml b/res/layout/apps_empty_view.xml
new file mode 100644
index 0000000..8408077
--- /dev/null
+++ b/res/layout/apps_empty_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/empty_text"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:paddingTop="24dp"
+    android:paddingBottom="24dp"
+    android:paddingRight="@dimen/apps_grid_view_start_margin"
+    android:textSize="16sp"
+    android:textColor="#4c4c4c"
+    android:focusable="false" />
+
diff --git a/res/layout/apps_grid_row_icon_view.xml b/res/layout/apps_grid_row_icon_view.xml
new file mode 100644
index 0000000..81e74b9
--- /dev/null
+++ b/res/layout/apps_grid_row_icon_view.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.launcher3.BubbleTextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    style="@style/WorkspaceIcon.AppsCustomize"
+    android:id="@+id/icon"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="left|center_vertical"
+    android:paddingTop="8dp"
+    android:paddingBottom="8dp"
+    android:focusable="true"
+    android:background="@drawable/focusable_view_bg"
+    launcher:deferShadowGeneration="true" />
+
diff --git a/res/layout/apps_list_row_icon_view.xml b/res/layout/apps_list_row_icon_view.xml
new file mode 100644
index 0000000..867dbdc
--- /dev/null
+++ b/res/layout/apps_list_row_icon_view.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<com.android.launcher3.BubbleTextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    style="@style/WorkspaceIcon.AppsCustomize"
+    android:id="@+id/application_icon"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:background="@drawable/focusable_view_bg"
+    launcher:iconPaddingOverride="24dp"
+    launcher:textSizeOverride="16dp"
+    launcher:layoutHorizontal="true"
+    launcher:deferShadowGeneration="true" />
diff --git a/res/layout/apps_list_row_view.xml b/res/layout/apps_list_row_view.xml
new file mode 100644
index 0000000..e80285b
--- /dev/null
+++ b/res/layout/apps_list_row_view.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/apps_view_row_height"
+    android:orientation="horizontal"
+    android:focusable="true"
+    android:background="@drawable/focusable_view_bg"
+    android:descendantFocusability="afterDescendants">
+    <TextView
+        android:id="@+id/section"
+        android:layout_width="64dp"
+        android:layout_height="match_parent"
+        android:paddingLeft="16dp"
+        android:gravity="start|center_vertical"
+        android:textColor="@color/apps_view_section_text_color"
+        android:textSize="@dimen/apps_view_section_text_size"
+        android:focusable="false" />
+</LinearLayout>
diff --git a/res/layout/apps_list_view.xml b/res/layout/apps_list_view.xml
new file mode 100644
index 0000000..dfb7b58
--- /dev/null
+++ b/res/layout/apps_list_view.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/apps_list"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:elevation="15dp"
+    android:visibility="gone"
+    android:focusableInTouchMode="true">
+    <EditText
+        android:id="@+id/app_search_box"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="16dp"
+        android:hint="@string/apps_view_search_bar_hint"
+        android:maxLines="1"
+        android:singleLine="true"
+        android:scrollHorizontally="true"
+        android:gravity="fill_horizontal"
+        android:textSize="16sp"
+        android:textColor="#4c4c4c"
+        android:textColorHint="#9c9c9c"
+        android:imeOptions="actionDone|flagNoExtractUi"
+        android:background="@drawable/apps_search_bg"
+        android:elevation="4dp" />
+    <com.android.launcher3.AppsContainerRecyclerView
+        android:id="@+id/apps_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:paddingTop="12dp"
+        android:paddingBottom="12dp"
+        android:clipToPadding="false"
+        android:focusable="true"
+        android:descendantFocusability="afterDescendants" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/apps_reveal_view.xml b/res/layout/apps_reveal_view.xml
new file mode 100644
index 0000000..2951ea4
--- /dev/null
+++ b/res/layout/apps_reveal_view.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/apps_view_transition_overlay"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:elevation="15dp"
+    android:visibility="invisible"
+    android:focusable="false" />
\ No newline at end of file
diff --git a/res/layout/apps_view.xml b/res/layout/apps_view.xml
new file mode 100644
index 0000000..7f09f77
--- /dev/null
+++ b/res/layout/apps_view.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<!-- The top and bottom paddings are defined in this container, but since we want
+     the list view to span the full width (for touch interception purposes), we
+     will bake the left/right padding into that view's background itself. -->
+<com.android.launcher3.AppsContainerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/apps_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:descendantFocusability="afterDescendants">
+    <include
+        layout="@layout/apps_reveal_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+    <include
+        layout="@layout/apps_list_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+</com.android.launcher3.AppsContainerView>
\ No newline at end of file
diff --git a/res/layout/page_indicator_marker.xml b/res/layout/page_indicator_marker.xml
index 686d275..564a958 100644
--- a/res/layout/page_indicator_marker.xml
+++ b/res/layout/page_indicator_marker.xml
@@ -16,8 +16,8 @@
 <com.android.launcher3.PageIndicatorMarker
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
-    android:layout_width="16dp"
-    android:layout_height="16dp"
+    android:layout_width="12dp"
+    android:layout_height="12dp"
     android:layout_gravity="center_vertical">
     <ImageView
         android:id="@+id/inactive"
diff --git a/res/layout/search_drop_target_bar.xml b/res/layout/search_drop_target_bar.xml
index 0d7167e..9b0da1d 100644
--- a/res/layout/search_drop_target_bar.xml
+++ b/res/layout/search_drop_target_bar.xml
@@ -45,6 +45,19 @@
             style="@style/DropTargetButtonContainer"
             android:layout_weight="1" >
 
+            <!-- Uninstall target -->
+
+            <com.android.launcher3.UninstallDropTarget
+                android:id="@+id/uninstall_target_text"
+                style="@style/DropTargetButton"
+                android:drawableStart="@drawable/uninstall_target_selector"
+                android:text="@string/delete_target_uninstall_label" />
+        </FrameLayout>
+
+        <FrameLayout
+            style="@style/DropTargetButtonContainer"
+            android:layout_weight="1" >
+
             <!-- Info target -->
 
             <com.android.launcher3.InfoDropTarget
diff --git a/res/layout/user_folder.xml b/res/layout/user_folder.xml
index ed8d43e..cd3a051 100644
--- a/res/layout/user_folder.xml
+++ b/res/layout/user_folder.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 The Android Open Source Project
+<!--
+     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.
@@ -14,42 +15,63 @@
      limitations under the License.
 -->
 
-<com.android.launcher3.Folder
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.launcher3.Folder xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:orientation="vertical"
-    android:background="@drawable/quantum_panel">
+    android:background="@drawable/quantum_panel"
+    android:orientation="vertical" >
 
-    <ScrollView
-        android:id="@+id/scroll_view"
+    <FrameLayout
+        android:id="@+id/folder_content_wrapper"
         android:layout_width="match_parent"
-        android:layout_height="match_parent">
-      <com.android.launcher3.CellLayout
-          android:id="@+id/folder_content"
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:cacheColorHint="#ff333333"
-          android:hapticFeedbackEnabled="false" />
-    </ScrollView>
+        android:layout_height="match_parent" >
 
-    <com.android.launcher3.FolderEditText
-        android:id="@+id/folder_name"
+        <!-- Actual size of the indicator doesn't matter as it is scaled to match the view size -->
+
+        <com.android.launcher3.FocusIndicatorView
+            android:id="@+id/focus_indicator"
+            android:layout_width="20dp"
+            android:layout_height="20dp" />
+
+        <com.android.launcher3.FolderPagedView
+            android:id="@+id/folder_content"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            launcher:pageIndicator="@+id/folder_page_indicator" />
+    </FrameLayout>
+
+    <LinearLayout
+        android:id="@+id/folder_footer"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_centerHorizontal="true"
-        android:paddingTop="@dimen/folder_name_padding"
-        android:paddingBottom="@dimen/folder_name_padding"
-        android:background="#00000000"
-        android:hint="@string/folder_hint_text"
-        android:textSize="14sp"
-        android:textColor="#ff777777"
-        android:textColorHint="#ff808080"
-        android:textColorHighlight="#ffCCCCCC"
-        android:textCursorDrawable="@null"
-        android:gravity="center_horizontal"
-        android:singleLine="true"
-        android:imeOptions="flagNoExtractUi"
-        android:fontFamily="sans-serif-condensed"/>
-</com.android.launcher3.Folder>
+        android:orientation="vertical" >
+
+        <include
+            android:id="@+id/folder_page_indicator"
+            android:layout_width="wrap_content"
+            android:layout_height="12dp"
+            android:layout_gravity="center_horizontal"
+            layout="@layout/page_indicator" />
+
+        <com.android.launcher3.FolderEditText
+            android:id="@+id/folder_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:background="#00000000"
+            android:fontFamily="sans-serif-condensed"
+            android:gravity="center_horizontal"
+            android:hint="@string/folder_hint_text"
+            android:imeOptions="flagNoExtractUi"
+            android:paddingBottom="@dimen/folder_name_padding"
+            android:paddingTop="@dimen/folder_name_padding"
+            android:singleLine="true"
+            android:textColor="#ff777777"
+            android:textColorHighlight="#ffCCCCCC"
+            android:textColorHint="#ff808080"
+            android:textCursorDrawable="@null"
+            android:textSize="14sp" />
+    </LinearLayout>
+
+</com.android.launcher3.Folder>
\ No newline at end of file
diff --git a/res/layout/widget_cell.xml b/res/layout/widget_cell.xml
new file mode 100644
index 0000000..f53b74e
--- /dev/null
+++ b/res/layout/widget_cell.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.launcher3.widget.WidgetCell
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto"
+    android:layout_width="@dimen/widget_preview_container_width"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:paddingTop="@dimen/widget_preview_padding_top"
+    android:orientation="vertical"
+    android:background="@drawable/focusable_view_bg"
+    android:focusable="true">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingTop="@dimen/widget_preview_label_vertical_padding"
+        android:paddingBottom="@dimen/widget_preview_label_vertical_padding"
+        android:paddingLeft="@dimen/widget_preview_label_horizontal_padding"
+        android:paddingRight="@dimen/widget_preview_label_horizontal_padding"
+        android:orientation="horizontal">
+
+        <!-- The name of the widget. -->
+        <TextView
+            android:id="@+id/widget_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="start"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:fadingEdge="horizontal"
+
+            android:textColor="#FFFFFFFF"
+            android:textSize="12sp"
+            android:textAlignment="viewStart"
+            android:fontFamily="sans-serif-condensed"
+            android:shadowRadius="2.0"
+            android:shadowColor="#B0000000" />
+
+        <!-- The original dimensions of the widget (can't be the same text as above due to different
+             style. -->
+        <TextView
+            android:id="@+id/widget_dims"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="5dp"
+            android:layout_marginLeft="5dp"
+            android:layout_weight="0"
+            android:gravity="start"
+
+            android:textColor="#FFFFFFFF"
+            android:textSize="12sp"
+            android:textAlignment="viewStart"
+            android:fontFamily="sans-serif-condensed"
+            android:shadowRadius="2.0"
+            android:shadowColor="#B0000000" />
+    </LinearLayout>
+
+    <!-- The image of the widget. -->
+    <com.android.launcher3.widget.WidgetImageView
+        android:id="@+id/widget_preview"
+        style="@style/WidgetImageView"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:scaleType="matrix" />
+
+</com.android.launcher3.widget.WidgetCell>
diff --git a/res/layout/widgets_list_row_view.xml b/res/layout/widgets_list_row_view.xml
new file mode 100644
index 0000000..017b450
--- /dev/null
+++ b/res/layout/widgets_list_row_view.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/widgets_cell_list_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:layout_marginLeft="8dp"
+    android:layout_marginRight="8dp"
+    android:layout_marginTop="8dp"
+    android:layout_marginBottom="8dp"
+    android:focusable="true"
+    android:background="@drawable/focusable_view_bg"
+    android:descendantFocusability="afterDescendants">
+
+    <!-- Section info -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:focusable="true"
+        android:background="@drawable/focusable_view_bg"
+        android:descendantFocusability="afterDescendants">
+        <ImageView
+            android:id="@+id/section_image"
+            android:layout_width="@dimen/widget_section_height"
+            android:layout_height="@dimen/widget_section_height"
+            android:paddingLeft="@dimen/widget_section_icon_padding"
+            android:paddingRight="@dimen/widget_section_icon_padding"
+            android:paddingTop="@dimen/widget_section_icon_padding"
+            android:paddingBottom="@dimen/widget_section_icon_padding"
+            android:background="@color/widget_text_panel"
+        />
+        <TextView
+            android:id="@+id/section"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/widget_section_height"
+            android:paddingTop="8dp"
+            android:paddingLeft="16dp"
+            android:paddingRight="16dp"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:gravity="start|center_vertical"
+            android:textColor="@color/widgets_view_section_text_color"
+            android:background="@color/widget_text_panel"
+            android:textSize="20sp"
+            android:focusable="false" />
+    </LinearLayout>
+
+    <!--  Widget list -->
+    <RelativeLayout 
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="end"
+        >
+        <!-- TODO(hyunyoungs): replace the indicator with actual assets. -->
+        <FrameLayout
+            android:id="@+id/scrollable_indicator"
+            android:layout_gravity="center_vertical"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/ic_pageindicator_default"
+            android:visibility="invisible"
+            />
+        <HorizontalScrollView
+            android:id="@+id/widgets_scroll_container"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/widget_cell_height"
+            android:scrollbars="none" >
+            <LinearLayout
+                android:id="@+id/widgets_cell_list"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal" />
+        </HorizontalScrollView>
+    </RelativeLayout>
+</LinearLayout>
diff --git a/res/layout/widgets_view.xml b/res/layout/widgets_view.xml
new file mode 100644
index 0000000..0800f59
--- /dev/null
+++ b/res/layout/widgets_view.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<!-- The top and bottom paddings are defined in this container, but since we want
+     the list view to span the full width (for touch interception purposes), we
+     will bake the left/right padding into that view's background itself. -->
+<com.android.launcher3.widget.WidgetsContainerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/widgets_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:paddingTop="@dimen/widget_container_inset"
+    android:paddingBottom="@dimen/widget_container_inset"
+    android:descendantFocusability="afterDescendants">
+
+    <FrameLayout
+        android:id="@+id/widgets_reveal_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:visibility="invisible"
+        android:focusable="false" />
+
+    <LinearLayout
+        android:id="@+id/widgets_content"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipChildren="false"
+        android:orientation="vertical">
+
+        <android.support.v7.widget.RecyclerView
+                android:id="@+id/widgets_list_view"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:background="@drawable/quantum_panel_dark"/>
+    </LinearLayout>
+</com.android.launcher3.widget.WidgetsContainerView>
\ No newline at end of file
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 2015158..2d3c633 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -199,10 +199,6 @@
     <skip />
     <!-- no translation found for folder_name_format (6629239338071103179) -->
     <skip />
-    <string name="custom_workspace_cling_title_1" msgid="3750880082935033085"></string>
-    <string name="custom_workspace_cling_description_1" msgid="939966842147696724"></string>
-    <string name="custom_workspace_cling_title_2" msgid="662588444436552198"></string>
-    <string name="custom_workspace_cling_description_2" msgid="8097921091798539310"></string>
     <!-- no translation found for widget_button_text (2880537293434387943) -->
     <skip />
     <!-- no translation found for wallpaper_button_text (8404103075899945851) -->
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 3b0deb8..a4955ce 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -23,13 +23,11 @@
     <string name="home" msgid="5921706419368316758">"Kodu"</string>
     <string name="uid_name" msgid="3371120195364560632">"Androidi tuumrakendused"</string>
     <string name="folder_name" msgid="8551881338202938211"></string>
-    <string name="chooser_wallpaper" msgid="6063168087625352235">"Taustapildi valimiskoht:"</string>
     <string name="wallpaper_instructions" msgid="4215640646180727542">"Määra taustapilt"</string>
     <string name="pick_wallpaper" msgid="5630222540525626723">"Taustapildid"</string>
     <string name="activity_not_found" msgid="217823393239365967">"Rakendus pole installitud."</string>
     <string name="widgets_tab_label" msgid="9145860100000983599">"Vidinad"</string>
     <string name="long_press_widget_to_add" msgid="7395697462851217506">"Vidina valimiseks puudutage seda pikalt."</string>
-    <string name="market" msgid="2652226429823445833">"Pood"</string>
     <string name="widget_dims_format" msgid="1386418557719032947">"%1$d × %2$d"</string>
     <string name="external_drop_widget_error" msgid="2285187188524172774">"Üksust ei saa sellele avaekraanile tuua."</string>
     <string name="external_drop_widget_pick_title" msgid="7040647073452295370">"Valige loomiseks vidin"</string>
@@ -52,23 +50,13 @@
     <string name="title_select_application" msgid="1793455815754848652">"Rakenduse valimine"</string>
     <string name="all_apps_button_label" msgid="2578400570124163469">"Rakendused"</string>
     <string name="all_apps_home_button_label" msgid="1022222300329398558">"Kodu"</string>
-    <string name="delete_zone_label_workspace" msgid="7153615831493049150">"Eemalda"</string>
     <string name="delete_zone_label_all_apps" msgid="6664588234817475108">"Desinstalli"</string>
     <string name="delete_target_label" msgid="665300185123139530">"Eemalda"</string>
     <string name="delete_target_uninstall_label" msgid="748894921183769150">"Desinstalli"</string>
     <string name="info_target_label" msgid="4019495079517426980">"Rakenduse teave"</string>
-    <string name="accessibility_search_button" msgid="816822994629942611">"Otsing"</string>
-    <string name="accessibility_voice_search_button" msgid="3938249215065842475">"Häälotsing"</string>
     <string name="accessibility_all_apps_button" msgid="8803738611398979849">"Rakendused"</string>
     <string name="accessibility_delete_button" msgid="3628162007991023603">"Eemalda"</string>
     <string name="delete_zone_label_all_apps_system_app" msgid="3683920959591819044">"Desinstalli värskendus"</string>
-    <string name="menu_add" msgid="3065046628354640854">"Lisa"</string>
-    <string name="menu_manage_apps" msgid="2308685199463588895">"Rakenduste haldamine"</string>
-    <string name="menu_wallpaper" msgid="5837429080911269832">"Taustapilt"</string>
-    <string name="menu_search" msgid="4826514464423239041">"Otsing"</string>
-    <string name="menu_notifications" msgid="6424587053194766192">"Teadistused"</string>
-    <string name="menu_settings" msgid="3946232973327980394">"Süsteemiseaded"</string>
-    <string name="menu_help" msgid="4901160661634590633">"Abi"</string>
     <string name="cab_menu_delete_app" msgid="4089398025537640349">"Rakenduse desinstallimine"</string>
     <string name="cab_menu_app_info" msgid="914548323652698884">"Rakenduse üksikasjad"</string>
     <string name="cab_app_selection_text" msgid="6378522164293415735">"Valitud on 1 rakendus"</string>
@@ -94,11 +82,7 @@
     <string name="apps_customize_widgets_scroll_format" msgid="5383009742241717437">"Vidinate leht %1$d/%2$d"</string>
     <string name="workspace_cling_title" msgid="738396473989890567">"Tunne end nagu kodus"</string>
     <string name="workspace_cling_move_item" msgid="791013895761065070">"Võite panna oma lemmikrakendused siia."</string>
-    <string name="workspace_cling_open_all_apps" msgid="2459977609848572588">"Kõikide oma rakenduste nägemiseks puudutage ringi."</string>
-    <string name="all_apps_cling_title" msgid="2559734712581447107">"Valige mõned rakendused"</string>
-    <string name="all_apps_cling_add_item" msgid="5665035103260318891">"Rakenduse lisamiseks avakuvale puudutage seda pikalt."</string>
     <string name="folder_cling_title" msgid="4308949882377840953">"Korraldage oma rakendused kaustadesse"</string>
-    <string name="folder_cling_move_item" msgid="270598675060435169">"Rakenduse liigutamiseks pange sõrm rakendusele ja hoidke seda."</string>
     <string name="folder_cling_create_folder" msgid="8352867485656129478">"Avakuval uue kausta tegemiseks virnastage üks rakendus teisele."</string>
     <string name="cling_dismiss" msgid="2780907108735868381">"OK"</string>
     <string name="folder_opened" msgid="1262064100943801533">"Kaust on avatud, <xliff:g id="WIDTH">%1$d</xliff:g> x <xliff:g id="HEIGHT">%2$d</xliff:g>"</string>
@@ -107,8 +91,4 @@
     <string name="folder_closed" msgid="3130534551370511932">"Kaust suletud"</string>
     <string name="folder_renamed" msgid="7951233572858053642">"Kausta uus nimi: <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format" msgid="3051680259794759037">"<xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="custom_workspace_cling_title_1" msgid="1433009175359948587"></string>
-    <string name="custom_workspace_cling_description_1" msgid="6875529190849858047"></string>
-    <string name="custom_workspace_cling_title_2" msgid="5516006164661020362"></string>
-    <string name="custom_workspace_cling_description_2" msgid="2758258454975288377"></string>
 </resources>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index 1b34181..06a9984 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -18,9 +18,4 @@
 <!-- QSB -->
     <dimen name="toolbar_button_vertical_padding">8dip</dimen>
     <dimen name="toolbar_button_horizontal_padding">0dip</dimen>
-
-<!-- AppsCustomize -->
-    <dimen name="apps_customize_tab_bar_height">42dp</dimen>
-    <integer name="apps_customize_widget_cell_count_x">3</integer>
-    <integer name="apps_customize_widget_cell_count_y">2</integer>
 </resources>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index f50a5db..248ca15 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -23,13 +23,11 @@
     <string name="home" msgid="5921706419368316758">"Laman Utama"</string>
     <string name="uid_name" msgid="3371120195364560632">"Apl Teras Android"</string>
     <string name="folder_name" msgid="8551881338202938211"></string>
-    <string name="chooser_wallpaper" msgid="6063168087625352235">"Pilih kertas dinding dari"</string>
     <string name="wallpaper_instructions" msgid="4215640646180727542">"Tetapkan kertas dinding"</string>
     <string name="pick_wallpaper" msgid="5630222540525626723">"Kertas dinding"</string>
     <string name="activity_not_found" msgid="217823393239365967">"Aplikasi tidak dipasang."</string>
     <string name="widgets_tab_label" msgid="9145860100000983599">"Widget"</string>
     <string name="long_press_widget_to_add" msgid="7395697462851217506">"Sentuh &amp; tahan untuk mengambil widget."</string>
-    <string name="market" msgid="2652226429823445833">"Kedai"</string>
     <string name="widget_dims_format" msgid="1386418557719032947">"%1$d × %2$d"</string>
     <string name="external_drop_widget_error" msgid="2285187188524172774">"Tidak dapat melepaskan item pada skrin Utama ini."</string>
     <string name="external_drop_widget_pick_title" msgid="7040647073452295370">"Pilih widget untuk dibuat"</string>
@@ -52,23 +50,13 @@
     <string name="title_select_application" msgid="1793455815754848652">"Pilih aplikasi"</string>
     <string name="all_apps_button_label" msgid="2578400570124163469">"Apl"</string>
     <string name="all_apps_home_button_label" msgid="1022222300329398558">"Laman Utama"</string>
-    <string name="delete_zone_label_workspace" msgid="7153615831493049150">"Alih keluar"</string>
     <string name="delete_zone_label_all_apps" msgid="6664588234817475108">"Nyahpasang"</string>
     <string name="delete_target_label" msgid="665300185123139530">"Alih keluar"</string>
     <string name="delete_target_uninstall_label" msgid="748894921183769150">"Nyahpasang"</string>
     <string name="info_target_label" msgid="4019495079517426980">"Maklumat apl"</string>
-    <string name="accessibility_search_button" msgid="816822994629942611">"Carian"</string>
-    <string name="accessibility_voice_search_button" msgid="3938249215065842475">"Carian Suara"</string>
     <string name="accessibility_all_apps_button" msgid="8803738611398979849">"Aplikasi"</string>
     <string name="accessibility_delete_button" msgid="3628162007991023603">"Alih keluar"</string>
     <string name="delete_zone_label_all_apps_system_app" msgid="3683920959591819044">"Nyahpasang kemas kini"</string>
-    <string name="menu_add" msgid="3065046628354640854">"Tambah"</string>
-    <string name="menu_manage_apps" msgid="2308685199463588895">"Mengurus apl"</string>
-    <string name="menu_wallpaper" msgid="5837429080911269832">"Kertas dinding"</string>
-    <string name="menu_search" msgid="4826514464423239041">"Cari"</string>
-    <string name="menu_notifications" msgid="6424587053194766192">"Pemberitahuan"</string>
-    <string name="menu_settings" msgid="3946232973327980394">"Tetapan sistem"</string>
-    <string name="menu_help" msgid="4901160661634590633">"Bantuan"</string>
     <string name="cab_menu_delete_app" msgid="4089398025537640349">"Nyahpasang aplikasi"</string>
     <string name="cab_menu_app_info" msgid="914548323652698884">"Butiran aplikasi"</string>
     <string name="cab_app_selection_text" msgid="6378522164293415735">"1 aplikasi dipilih"</string>
@@ -94,11 +82,7 @@
     <string name="apps_customize_widgets_scroll_format" msgid="5383009742241717437">"Halaman widget %1$d dari %2$d"</string>
     <string name="workspace_cling_title" msgid="738396473989890567">"Buat diri anda seperti di rumah"</string>
     <string name="workspace_cling_move_item" msgid="791013895761065070">"Anda boleh meletakkan aplikasi kegemaran anda di sini."</string>
-    <string name="workspace_cling_open_all_apps" msgid="2459977609848572588">"Untuk melihat semua aplikasi anda, sentuh bulatan."</string>
-    <string name="all_apps_cling_title" msgid="2559734712581447107">"Pilih beberapa aplikasi"</string>
-    <string name="all_apps_cling_add_item" msgid="5665035103260318891">"Untuk menambahkan aplikasi pada skrin Utama anda, sentuh &amp; tahankannya."</string>
     <string name="folder_cling_title" msgid="4308949882377840953">"Susun aplikasi anda dengan folder"</string>
-    <string name="folder_cling_move_item" msgid="270598675060435169">"Untuk memindahkan aplikasi, sentuh &amp; tahankannya."</string>
     <string name="folder_cling_create_folder" msgid="8352867485656129478">"Untuk membuat folder baharu pada skrin Utama anda, tindihkan satu aplikasi di atas yang lain."</string>
     <string name="cling_dismiss" msgid="2780907108735868381">"OK"</string>
     <string name="folder_opened" msgid="1262064100943801533">"Folder dibuka, <xliff:g id="WIDTH">%1$d</xliff:g> kali <xliff:g id="HEIGHT">%2$d</xliff:g>"</string>
@@ -107,8 +91,4 @@
     <string name="folder_closed" msgid="3130534551370511932">"Folder ditutup"</string>
     <string name="folder_renamed" msgid="7951233572858053642">"Folder dinamakan semula kepada <xliff:g id="NAME">%1$s</xliff:g>"</string>
     <string name="folder_name_format" msgid="3051680259794759037">"Folder: <xliff:g id="NAME">%1$s</xliff:g>"</string>
-    <string name="custom_workspace_cling_title_1" msgid="1433009175359948587"></string>
-    <string name="custom_workspace_cling_description_1" msgid="6875529190849858047"></string>
-    <string name="custom_workspace_cling_title_2" msgid="5516006164661020362"></string>
-    <string name="custom_workspace_cling_description_2" msgid="2758258454975288377"></string>
 </resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index 28679be..9ecd07dd 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -17,12 +17,16 @@
 <resources>
     <dimen name="app_icon_size">64dp</dimen>
 
+<!-- Apps view -->
+    <dimen name="apps_container_inset">24dp</dimen>
+    <dimen name="apps_grid_view_start_margin">64dp</dimen>
+    <dimen name="apps_view_section_text_size">26sp</dimen>
+    <dimen name="apps_view_row_height">76dp</dimen>
+
 <!-- AppsCustomize -->
-    <dimen name="apps_customize_tab_bar_height">60dp</dimen>
-    <dimen name="apps_customize_tab_bar_margin_top">8dp</dimen>
-    <dimen name="app_widget_preview_label_margin_top">8dp</dimen>
-    <dimen name="app_widget_preview_label_margin_left">@dimen/app_widget_preview_padding_left</dimen>
-    <dimen name="app_widget_preview_label_margin_right">@dimen/app_widget_preview_padding_right</dimen>
+    <dimen name="widget_preview_label_margin_top">8dp</dimen>
+    <dimen name="widget_preview_label_margin_left">@dimen/widget_preview_label_horizontal_padding</dimen>
+    <dimen name="widget_preview_label_margin_right">@dimen/widget_preview_label_horizontal_padding</dimen>
 
 <!-- Cling -->
     <dimen name="cling_migration_logo_height">400dp</dimen>
diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
index 8be9964..68fc1ec 100644
--- a/res/values-sw720dp/dimens.xml
+++ b/res/values-sw720dp/dimens.xml
@@ -21,11 +21,6 @@
     <dimen name="toolbar_button_vertical_padding">8dip</dimen>
     <dimen name="toolbar_button_horizontal_padding">8dip</dimen>
 
-    <!-- When dragging items on the workspace, the number of dps by which the position of
-     the drag view should be offset from the position of the original view. -->
-    <dimen name="dragViewOffsetX">0dp</dimen>
-    <dimen name="dragViewOffsetY">0dp</dimen>
-
 <!-- Cling -->
     <dimen name="cling_migration_content_margin">96dp</dimen>
     <dimen name="cling_migration_content_width">320dp</dimen>
diff --git a/res/values-v17/styles.xml b/res/values-v17/styles.xml
index 11d2a1f..3589e80 100644
--- a/res/values-v17/styles.xml
+++ b/res/values-v17/styles.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <style name="PagedViewWidgetImageView">
-        <item name="android:paddingStart">@dimen/app_widget_preview_padding_left</item>
+    <style name="WidgetImageView">
+        <item name="android:paddingStart">@dimen/widget_preview_horizontal_padding</item>
     </style>
 </resources>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 3331cde..a1f2845 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -18,6 +18,16 @@
 
 <resources>
 
+    <!-- BubbleTextView specific attributes. -->
+    <declare-styleable name="BubbleTextView">
+        <attr name="layoutHorizontal" format="boolean" />
+        <attr name="iconSizeOverride" format="dimension" />
+        <attr name="iconPaddingOverride" format="dimension" />
+        <attr name="textSizeOverride" format="dimension" />
+        <attr name="deferShadowGeneration" format="boolean" />
+        <attr name="customShadows" format="boolean" />
+    </declare-styleable>
+
     <!-- Page Indicator specific attributes. -->
     <declare-styleable name="PageIndicator">
         <attr name="windowSize" format="integer"  />
@@ -80,11 +90,6 @@
         <attr name="pageIndicator" format="reference" />
     </declare-styleable>
 
-    <declare-styleable name="BubbleTextView">
-        <!-- A spacing override for the icons within a page -->
-        <attr name="customShadows" format="boolean" />
-    </declare-styleable>
-
     <!-- AppsCustomizePagedView specific attributes.  These attributes are used to
          customize an AppsCustomizePagedView in xml files. -->
     <declare-styleable name="AppsCustomizePagedView">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 2daf9fe..3a06bd9 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -36,4 +36,10 @@
     <color name="outline_color">#FFFFFFFF</color>
     <color name="widget_text_panel">#FF374248</color>
 
+    <!-- Apps view -->
+    <color name="apps_view_scrollbar_thumb_color">#009688</color>
+    <color name="apps_view_section_text_color">#009688</color>
+
+    <!-- Widgetss view -->
+    <color name="widgets_view_section_text_color">#009688</color>
 </resources>
diff --git a/res/values/config.xml b/res/values/config.xml
index cbec512..1acace6 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -104,4 +104,5 @@
     <item type="id" name="action_uninstall" />
     <item type="id" name="action_info" />
     <item type="id" name="action_add_to_workspace" />
+    <item type="id" name="action_move" />
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 54689ec..cad60fb 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -46,19 +46,25 @@
     <dimen name="toolbar_button_vertical_padding">4dip</dimen>
     <dimen name="toolbar_button_horizontal_padding">12dip</dimen>
 
+<!-- Apps view -->
+    <dimen name="apps_container_width">0dp</dimen>
+    <dimen name="apps_container_height">0dp</dimen>
+    <dimen name="apps_container_inset">8dp</dimen>
+    <!-- Note: This needs to match the fixed insets for the search box -->
+    <dimen name="apps_container_fixed_bounds_inset">8dp</dimen>
+    <dimen name="apps_grid_view_start_margin">52dp</dimen>
+    <dimen name="apps_view_row_height">64dp</dimen>
+    <dimen name="apps_view_section_text_size">24sp</dimen>
+    <dimen name="apps_view_fast_scroll_bar_width">6dp</dimen>
+    <dimen name="apps_view_fast_scroll_bar_min_height">64dp</dimen>
+    <dimen name="apps_view_fast_scroll_scrubber_touch_inset">-16dp</dimen>
+    <dimen name="apps_view_fast_scroll_popup_size">64dp</dimen>
+    <dimen name="apps_view_fast_scroll_text_size">40dp</dimen>
+
 <!-- AllApps/Customize/AppsCustomize -->
-    <!-- The height of the tab bar - if this changes, we should update the
-         external icon width/height above to compensate -->
-    <dimen name="apps_customize_tab_bar_height">52dp</dimen>
-    <dimen name="apps_customize_tab_bar_margin_top">0dp</dimen>
     <dimen name="app_icon_size">48dp</dimen>
     <dimen name="apps_customize_horizontal_padding">0dp</dimen>
 
-    <!-- The AppsCustomize page indicator -->
-    <dimen name="apps_customize_page_indicator_height">12dp</dimen>
-    <dimen name="apps_customize_page_indicator_margin">4dp</dimen>
-    <dimen name="apps_customize_page_indicator_offset">16dp</dimen>
-
     <!-- Drag padding to add to the bottom of drop targets -->
     <dimen name="drop_target_drag_padding">14dp</dimen>
     <dimen name="drop_target_text_size">14sp</dimen>
@@ -68,20 +74,24 @@
          or right while you're dragging. -->
     <dimen name="scroll_zone">20dp</dimen>
 
-    <!-- When dragging items on the workspace, the number of dps by which the position of
-     the drag view should be offset from the position of the original view. -->
-    <dimen name="dragViewOffsetX">0dp</dimen>
-    <dimen name="dragViewOffsetY">0dp</dimen>
     <!--  When dragging an item, how much bigger (fixed dps) the dragged view
           should be. If 0, it will not be scaled at all. -->
     <dimen name="dragViewScale">12dp</dimen>
 
-    <!-- Padding applied to AppWidget previews -->
-    <dimen name="app_widget_preview_padding_left">16dp</dimen>
-    <dimen name="app_widget_preview_padding_right">16dp</dimen>
-    <dimen name="app_widget_preview_padding_top">32dp</dimen>
-    <dimen name="app_widget_preview_label_vertical_padding">8dp</dimen>
-    <dimen name="app_widget_preview_label_horizontal_padding">8dp</dimen>
+<!-- Widget tray -->
+    <dimen name="widget_container_inset">8dp</dimen>
+    <dimen name="widget_preview_size">140dp</dimen>
+    <dimen name="widget_preview_padding_top">8dp</dimen>
+    <dimen name="widget_preview_label_vertical_padding">8dp</dimen>
+    <dimen name="widget_preview_label_horizontal_padding">8dp</dimen>
+    <dimen name="widget_preview_horizontal_padding">8dp</dimen>
+
+    <dimen name="widget_section_height">52dp</dimen>
+    <dimen name="widget_section_icon_padding">8dp</dimen>
+
+    <!-- Equation: widget_preview_size + 2 * widget_preview_padding_horizontal -->
+    <dimen name="widget_preview_container_width">156dp</dimen>
+    <dimen name="widget_cell_height">160dp</dimen>
 
     <!-- Padding applied to shortcut previews -->
     <dimen name="shortcut_preview_padding_left">0dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8b7e6c1..52306e3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -38,6 +38,8 @@
     <string name="uid_name">Android Core Apps</string>
     <!-- Default folder name -->
     <string name="folder_name"></string>
+    <!-- Work folder name -->
+    <string name="work_folder_name">Work</string>
     <!-- Displayed when user selects a shortcut for an app that was uninstalled [CHAR_LIMIT=none]-->
     <string name="activity_not_found">App isn\'t installed.</string>
     <!-- Displayed when user selects a shortcut for an app that is current not available [CHAR_LIMIT=none]-->
@@ -71,6 +73,14 @@
          drop if there are multiple choices. [CHAR_LIMIT=35] -->
     <string name="external_drop_widget_pick_title">Choose widget to create</string>
 
+    <!-- Apps view -->
+    <!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
+    <string name="apps_view_search_bar_hint">Search Apps</string>
+    <!-- Loading apps text. [CHAR_LIMIT=50] -->
+    <string name="loading_apps_message">Loading Apps...</string>
+    <!-- No-search-results text. [CHAR_LIMIT=50] -->
+    <string name="apps_view_no_search_results">No Apps found matching \"<xliff:g id="query" example="Android">%1$s</xliff:g>\"</string>
+
     <!-- Folders -->
     <skip />
     <!-- Label of Folder name field in Rename folder dialog box -->
@@ -81,6 +91,8 @@
     <string name="rename_action">OK</string>
     <!-- Buttons in Rename folder dialog box -->
     <string name="cancel_action">Cancel</string>
+    <!-- Label for button to sort folder contents. [CHAR_LIMIT=10] -->
+    <string name="sort_alphabetical">A-Z</string>
 
     <!-- Shortcuts -->
     <skip />
@@ -103,8 +115,6 @@
     <string name="invalid_hotseat_item">This widget is too large for the Favorites tray</string>
     <!-- Message displayed when a shortcut is created by an external application -->
     <string name="shortcut_installed">Shortcut \"<xliff:g id="name" example="Browser">%s</xliff:g>\" created.</string>
-    <!-- Message displayed when a shortcut is uninstalled by an external application -->
-    <string name="shortcut_uninstalled">Shortcut \"<xliff:g id="name" example="Browser">%s</xliff:g>\" was removed.</string>
     <!-- Message displayed when an external application attemps to create a shortcut that already exists -->
     <string name="shortcut_duplicate">Shortcut \"<xliff:g id="name" example="Browser">%s</xliff:g>\" already exists.</string>
 
@@ -166,11 +176,6 @@
     <string name="permdesc_install_shortcut">Allows an app to add
         shortcuts without user intervention.</string>
     <!-- Permission short label -->
-    <string name="permlab_uninstall_shortcut">uninstall shortcuts</string>
-    <!-- Permission description -->
-    <string name="permdesc_uninstall_shortcut">Allows the app to remove
-        shortcuts without user intervention.</string>
-    <!-- Permission short label -->
     <string name="permlab_read_settings">read Home settings and shortcuts</string>
     <!-- Permission description -->
     <string name="permdesc_read_settings">Allows the app to read the settings and
@@ -303,6 +308,34 @@
     </string>
 
 <!-- Strings for accessibility actions -->
-    <!-- Accessibility action to add an app to workspace. [CHAR_LIMIT=30] -->
+    <!-- Accessibility action to add an app to workspace. [CHAR_LIMIT=30] [DO NOT TRANSLATE] -->
     <string name="action_add_to_workspace">Add To Workspace</string>
+
+    <!-- Accessibility confirmation for item added to workspace [DO NOT TRANSLATE] -->
+    <string name="item_added_to_workspace">Item added to workspace</string>
+
+    <!-- Accessibility confirmation for item removed [DO NOT TRANSLATE] -->
+    <string name="item_removed_from_workspace">Item removed from workspace</string>
+
+    <!-- Accessibility action to move an item on the workspace. [CHAR_LIMIT=30] [DO NOT TRANSLATE] -->
+    <string name="action_move">Move Item</string>
+
+    <!-- Accessibility description to move item to empty cell. [DO NOT TRANSLATE] -->
+    <string name="move_to_empty_cell">Move to empty cell <xliff:g id="number" example="1">%1$s</xliff:g>, <xliff:g id="number" example="1">%2$s</xliff:g></string>
+
+    <!-- Accessibility confirmation for item move [DO NOT TRANSLATE]-->
+    <string name="item_moved">Item moved</string>
+
+    <!-- Accessibility description to move item into an existing folder. [DO NOT TRANSLATE]-->
+    <string name="add_to_folder">Add to folder: <xliff:g id="name" example="Games">%1$s</xliff:g></string>
+
+    <!-- Accessibility confirmation for item added to folder [DO NOT TRANSLATE] -->
+    <string name="added_to_folder">Item added to folder</string>
+
+    <!-- Accessibility description to create folder with another item. [DO NOT TRANSLATE] -->
+    <string name="create_folder_with">Create folder with: <xliff:g id="name" example="Game">%1$s</xliff:g></string>
+
+    <!-- Accessibility confirmation for folder created [DO NOT TRANSLATE] -->
+    <string name="folder_created">Folder created</string>
+
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 77798f1..16d4cbe 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -91,8 +91,8 @@
     </style>
 
     <!-- Overridden in device overlays -->
-    <style name="PagedViewWidgetImageView">
-        <item name="android:paddingLeft">@dimen/app_widget_preview_padding_left</item>
+    <style name="WidgetImageView">
+        <item name="android:paddingLeft">@dimen/widget_preview_horizontal_padding</item>
     </style>
 
 </resources>
diff --git a/res/xml/app_target_browser.xml b/res/xml/app_target_browser.xml
new file mode 100644
index 0000000..d7c3ed5
--- /dev/null
+++ b/res/xml/app_target_browser.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+    <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
+    <favorite launcher:uri="http://www.example.com/" />
+
+</resolve>
\ No newline at end of file
diff --git a/res/xml/app_target_camera.xml b/res/xml/app_target_camera.xml
new file mode 100644
index 0000000..f65a2b1
--- /dev/null
+++ b/res/xml/app_target_camera.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+    <favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
+    <favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
+
+</resolve>
\ No newline at end of file
diff --git a/res/xml/app_target_email.xml b/res/xml/app_target_email.xml
new file mode 100644
index 0000000..44f0a40
--- /dev/null
+++ b/res/xml/app_target_email.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+    <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
+    <favorite launcher:uri="mailto:" />
+
+</resolve>
\ No newline at end of file
diff --git a/res/xml/app_target_gallery.xml b/res/xml/app_target_gallery.xml
new file mode 100644
index 0000000..c9d3492
--- /dev/null
+++ b/res/xml/app_target_gallery.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+    <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
+    <favorite launcher:uri="#Intent;type=images/*;end" />
+
+</resolve>
\ No newline at end of file
diff --git a/res/xml/app_target_messenger.xml b/res/xml/app_target_messenger.xml
new file mode 100644
index 0000000..278eb5c
--- /dev/null
+++ b/res/xml/app_target_messenger.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+    <favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" />
+    <favorite launcher:uri="sms:" />
+    <favorite launcher:uri="smsto:" />
+    <favorite launcher:uri="mms:" />
+    <favorite launcher:uri="mmsto:" />
+
+</resolve>
\ No newline at end of file
diff --git a/res/xml/app_target_phone.xml b/res/xml/app_target_phone.xml
new file mode 100644
index 0000000..5d6ca31
--- /dev/null
+++ b/res/xml/app_target_phone.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<resolve xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+
+    <favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" />
+    <favorite launcher:uri="tel:123" />
+    <favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" />
+
+</resolve>
\ No newline at end of file
diff --git a/src/com/android/launcher3/AllAppsList.java b/src/com/android/launcher3/AllAppsList.java
index 72c6693..dd646bb 100644
--- a/src/com/android/launcher3/AllAppsList.java
+++ b/src/com/android/launcher3/AllAppsList.java
@@ -98,14 +98,14 @@
                 user);
 
         for (LauncherActivityInfoCompat info : matches) {
-            add(new AppInfo(context, info, user, mIconCache, null));
+            add(new AppInfo(context, info, user, mIconCache));
         }
     }
 
     /**
      * Remove the apps for the given apk identified by packageName.
      */
-    public void removePackage(String packageName, UserHandleCompat user, boolean clearCache) {
+    public void removePackage(String packageName, UserHandleCompat user) {
         final List<AppInfo> data = this.data;
         for (int i = data.size() - 1; i >= 0; i--) {
             AppInfo info = data.get(i);
@@ -115,9 +115,6 @@
                 data.remove(i);
             }
         }
-        if (clearCache) {
-            mIconCache.remove(packageName, user);
-        }
     }
 
     /**
@@ -137,7 +134,6 @@
                         && packageName.equals(component.getPackageName())) {
                     if (!findActivity(matches, component)) {
                         removed.add(applicationInfo);
-                        mIconCache.remove(component, user);
                         data.remove(i);
                     }
                 }
@@ -150,10 +146,9 @@
                         info.getComponentName().getPackageName(), user,
                         info.getComponentName().getClassName());
                 if (applicationInfo == null) {
-                    add(new AppInfo(context, info, user, mIconCache, null));
+                    add(new AppInfo(context, info, user, mIconCache));
                 } else {
-                    mIconCache.remove(applicationInfo.componentName, user);
-                    mIconCache.getTitleAndIcon(applicationInfo, info, null);
+                    mIconCache.getTitleAndIcon(applicationInfo, info, true /* useLowResIcon */);
                     modified.add(applicationInfo);
                 }
             }
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
new file mode 100644
index 0000000..2ee5a62
--- /dev/null
+++ b/src/com/android/launcher3/AlphabeticalAppsList.java
@@ -0,0 +1,316 @@
+package com.android.launcher3;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * A private class to manage access to an app name comparator.
+ */
+class AppNameComparator {
+    private UserManagerCompat mUserManager;
+    private Comparator<AppInfo> mAppNameComparator;
+    private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
+
+    public AppNameComparator(Context context) {
+        final Collator collator = Collator.getInstance();
+        mUserManager = UserManagerCompat.getInstance(context);
+        mAppNameComparator = new Comparator<AppInfo>() {
+            public final int compare(AppInfo a, AppInfo b) {
+                // Order by the title
+                int result = collator.compare(a.title.toString().trim(),
+                        b.title.toString().trim());
+                if (result == 0) {
+                    // If two apps have the same title, then order by the component name
+                    result = a.componentName.compareTo(b.componentName);
+                    if (result == 0) {
+                        // If the two apps are the same component, then prioritize by the order that
+                        // the app user was created (prioritizing the main user's apps)
+                        if (UserHandleCompat.myUserHandle().equals(a.user)) {
+                            return -1;
+                        } else {
+                            Long aUserSerial = getAndCacheUserSerial(a.user);
+                            Long bUserSerial = getAndCacheUserSerial(b.user);
+                            return aUserSerial.compareTo(bUserSerial);
+                        }
+                    }
+                }
+                return result;
+            }
+        };
+    }
+
+    /**
+     * Returns a locale-aware comparator that will alphabetically order a list of applications.
+     */
+    public Comparator<AppInfo> getComparator() {
+        // Clear the user serial cache so that we get serials as needed in the comparator
+        mUserSerialCache.clear();
+        return mAppNameComparator;
+    }
+
+    /**
+     * Returns the user serial for this user, using a cached serial if possible.
+     */
+    private Long getAndCacheUserSerial(UserHandleCompat user) {
+        Long userSerial = mUserSerialCache.get(user);
+        if (userSerial == null) {
+            userSerial = mUserManager.getSerialNumberForUser(user);
+            mUserSerialCache.put(user, userSerial);
+        }
+        return userSerial;
+    }
+}
+
+/**
+ * The alphabetically sorted list of applications.
+ */
+public class AlphabeticalAppsList {
+
+    /**
+     * Info about a section in the alphabetic list
+     */
+    public static class SectionInfo {
+        // The name of this section
+        public String sectionName;
+        // The number of applications in this section
+        public int numAppsInSection;
+        // The first app AdapterItem for this section
+        public AdapterItem firstAppItem;
+
+        public SectionInfo(String name) {
+            sectionName = name;
+        }
+    }
+
+    /**
+     * Info about a particular adapter item (can be either section or app)
+     */
+    public static class AdapterItem {
+        // The index of this adapter item in the list
+        public int position;
+        // Whether or not the item at this adapter position is a section or not
+        public boolean isSectionHeader;
+        // The name of this section, or the section that this app is contained in
+        public String sectionName;
+        // The associated AppInfo, or null if this adapter item is a section
+        public AppInfo appInfo;
+        // The index of this app (not including sections), or -1 if this adapter item is a section
+        public int appIndex;
+
+        public static AdapterItem asSection(int pos, String name) {
+            AdapterItem item = new AdapterItem();
+            item.position = pos;
+            item.isSectionHeader = true;
+            item.sectionName = name;
+            item.appInfo = null;
+            item.appIndex = -1;
+            return item;
+        }
+
+        public static AdapterItem asApp(int pos, String sectionName, AppInfo appInfo, int appIndex) {
+            AdapterItem item = new AdapterItem();
+            item.position = pos;
+            item.isSectionHeader = false;
+            item.sectionName = sectionName;
+            item.appInfo = appInfo;
+            item.appIndex = appIndex;
+            return item;
+        }
+    }
+
+    /**
+     * A filter interface to limit the set of applications in the apps list.
+     */
+    public interface Filter {
+        public boolean retainApp(AppInfo info, String sectionName);
+    }
+
+    private List<AppInfo> mApps = new ArrayList<>();
+    private List<AppInfo> mFilteredApps = new ArrayList<>();
+    private List<AdapterItem> mSectionedFilteredApps = new ArrayList<>();
+    private List<SectionInfo> mSections = new ArrayList<>();
+    private RecyclerView.Adapter mAdapter;
+    private Filter mFilter;
+    private AlphabeticIndexCompat mIndexer;
+    private AppNameComparator mAppNameComparator;
+
+    public AlphabeticalAppsList(Context context) {
+        mIndexer = new AlphabeticIndexCompat(context);
+        mAppNameComparator = new AppNameComparator(context);
+    }
+
+    /**
+     * Sets the adapter to notify when this dataset changes.
+     */
+    public void setAdapter(RecyclerView.Adapter adapter) {
+        mAdapter = adapter;
+    }
+
+    /**
+     * Returns sections of all the current filtered applications.
+     */
+    public List<SectionInfo> getSections() {
+        return mSections;
+    }
+
+    /**
+     * Returns the current filtered list of applications broken down into their sections.
+     */
+    public List<AdapterItem> getAdapterItems() {
+        return mSectionedFilteredApps;
+    }
+
+    /**
+     * Returns the number of applications in this list.
+     */
+    public int getSize() {
+        return mFilteredApps.size();
+    }
+
+    /**
+     * Returns whether there are no filtered results.
+     */
+    public boolean hasNoFilteredResults() {
+        return (mFilter != null) && mFilteredApps.isEmpty();
+    }
+
+    /**
+     * Sets the current filter for this list of apps.
+     */
+    public void setFilter(Filter f) {
+        mFilter = f;
+        onAppsUpdated();
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Sets the current set of apps.
+     */
+    public void setApps(List<AppInfo> apps) {
+        Collections.sort(apps, mAppNameComparator.getComparator());
+        mApps.clear();
+        mApps.addAll(apps);
+        onAppsUpdated();
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Adds new apps to the list.
+     */
+    public void addApps(List<AppInfo> apps) {
+        // We add it in place, in alphabetical order
+        for (AppInfo info : apps) {
+            addApp(info);
+        }
+    }
+
+    /**
+     * Updates existing apps in the list
+     */
+    public void updateApps(List<AppInfo> apps) {
+        for (AppInfo info : apps) {
+            int index = mApps.indexOf(info);
+            if (index != -1) {
+                mApps.set(index, info);
+                onAppsUpdated();
+                mAdapter.notifyItemChanged(index);
+            } else {
+                addApp(info);
+            }
+        }
+    }
+
+    /**
+     * Removes some apps from the list.
+     */
+    public void removeApps(List<AppInfo> apps) {
+        for (AppInfo info : apps) {
+            int removeIndex = findAppByComponent(mApps, info);
+            if (removeIndex != -1) {
+                mApps.remove(removeIndex);
+                onAppsUpdated();
+                mAdapter.notifyDataSetChanged();
+            }
+        }
+    }
+
+    /**
+     * Finds the index of an app given a target AppInfo.
+     */
+    private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) {
+        ComponentName targetComponent = targetInfo.intent.getComponent();
+        int length = apps.size();
+        for (int i = 0; i < length; ++i) {
+            AppInfo info = apps.get(i);
+            if (info.user.equals(info.user)
+                    && info.intent.getComponent().equals(targetComponent)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Implementation to actually add an app to the alphabetic list
+     */
+    private void addApp(AppInfo info) {
+        int index = Collections.binarySearch(mApps, info, mAppNameComparator.getComparator());
+        if (index < 0) {
+            mApps.add(-(index + 1), info);
+            onAppsUpdated();
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    /**
+     * Updates internals when the set of apps are updated.
+     */
+    private void onAppsUpdated() {
+        // Recreate the filtered and sectioned apps (for convenience for the grid layout)
+        mFilteredApps.clear();
+        mSections.clear();
+        mSectionedFilteredApps.clear();
+        SectionInfo lastSectionInfo = null;
+        int position = 0;
+        int appIndex = 0;
+        for (AppInfo info : mApps) {
+            String sectionName = mIndexer.computeSectionName(info.title.toString().trim());
+
+            // Check if we want to retain this app
+            if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
+                continue;
+            }
+
+            // Create a new section if necessary
+            if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) {
+                lastSectionInfo = new SectionInfo(sectionName);
+                mSections.add(lastSectionInfo);
+
+                // Create a new section item
+                AdapterItem sectionItem = AdapterItem.asSection(position++, sectionName);
+                mSectionedFilteredApps.add(sectionItem);
+            }
+
+            // Create an app item
+            AdapterItem appItem = AdapterItem.asApp(position++, sectionName, info, appIndex++);
+            lastSectionInfo.numAppsInSection++;
+            if (lastSectionInfo.firstAppItem == null) {
+                lastSectionInfo.firstAppItem = appItem;
+            }
+            mSectionedFilteredApps.add(appItem);
+            mFilteredApps.add(info);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/AppInfo.java b/src/com/android/launcher3/AppInfo.java
index a66bac0..7c6b066 100644
--- a/src/com/android/launcher3/AppInfo.java
+++ b/src/com/android/launcher3/AppInfo.java
@@ -19,19 +19,15 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.ResolveInfo;
 import android.graphics.Bitmap;
 import android.util.Log;
 
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
-import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 
 /**
  * Represents an app in AllAppsView.
@@ -47,14 +43,19 @@
     /**
      * A bitmap version of the application icon.
      */
-    Bitmap iconBitmap;
+    public Bitmap iconBitmap;
+
+    /**
+     * Indicates whether we're using a low res icon
+     */
+    boolean usingLowResIcon;
 
     /**
      * The time at which the app was first installed.
      */
     long firstInstallTime;
 
-    ComponentName componentName;
+    public ComponentName componentName;
 
     static final int DOWNLOADED_FLAG = 1;
     static final int UPDATED_SYSTEM_APP_FLAG = 2;
@@ -77,13 +78,13 @@
      * Must not hold the Context.
      */
     public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user,
-            IconCache iconCache, HashMap<Object, CharSequence> labelCache) {
+            IconCache iconCache) {
         this.componentName = info.getComponentName();
         this.container = ItemInfo.NO_ID;
 
         flags = initFlags(info);
         firstInstallTime = info.getFirstInstallTime();
-        iconCache.getTitleAndIcon(this, info, labelCache);
+        iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */);
         intent = makeLaunchIntent(context, info, user);
         this.user = user;
     }
@@ -120,12 +121,15 @@
                 + " user=" + user + ")";
     }
 
+    /**
+     * Helper method used for debugging.
+     */
     public static void dumpApplicationInfoList(String tag, String label, ArrayList<AppInfo> list) {
         Log.d(tag, label + " size=" + list.size());
         for (AppInfo info: list) {
-            Log.d(tag, "   title=\"" + info.title + "\" iconBitmap="
-                    + info.iconBitmap + " firstInstallTime="
-                    + info.firstInstallTime);
+            Log.d(tag, "   title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap 
+                    + " firstInstallTime=" + info.firstInstallTime
+                    + " componentName=" + info.componentName.getPackageName());
         }
     }
 
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 880aaf1..5e7a012 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -90,5 +90,10 @@
                 }
             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
         }
+
+        LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+        if (app != null) {
+            app.reloadWorkspace();
+        }
     }
 }
diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java
new file mode 100644
index 0000000..16244ee
--- /dev/null
+++ b/src/com/android/launcher3/AppsContainerRecyclerView.java
@@ -0,0 +1,412 @@
+/*
+ * 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.launcher3;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import java.util.List;
+
+/**
+ * A RecyclerView with custom fastscroll support.  This is the main container for the all apps
+ * icons.
+ */
+public class AppsContainerRecyclerView extends RecyclerView
+        implements RecyclerView.OnItemTouchListener {
+
+    private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
+
+    private AlphabeticalAppsList mApps;
+    private int mNumAppsPerRow;
+
+    private Drawable mScrollbar;
+    private Drawable mFastScrollerBg;
+    private Rect mVerticalScrollbarBounds = new Rect();
+    private boolean mDraggingFastScroller;
+    private String mFastScrollSectionName;
+    private Paint mFastScrollTextPaint;
+    private Rect mFastScrollTextBounds = new Rect();
+    private float mFastScrollAlpha;
+    private int mDownX;
+    private int mDownY;
+    private int mLastX;
+    private int mLastY;
+    private int mScrollbarWidth;
+    private int mScrollbarMinHeight;
+    private int mScrollbarInset;
+
+    public AppsContainerRecyclerView(Context context) {
+        this(context, null);
+    }
+
+    public AppsContainerRecyclerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr);
+
+        Resources res = context.getResources();
+        int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size);
+        mScrollbar = res.getDrawable(R.drawable.apps_list_scrollbar_thumb);
+        mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg);
+        mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
+        mFastScrollTextPaint = new Paint();
+        mFastScrollTextPaint.setColor(Color.WHITE);
+        mFastScrollTextPaint.setAntiAlias(true);
+        mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
+                R.dimen.apps_view_fast_scroll_text_size));
+        mScrollbarWidth = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_width);
+        mScrollbarMinHeight =
+                res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_bar_min_height);
+        mScrollbarInset =
+                res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_scrubber_touch_inset);
+        setFastScrollerAlpha(getFastScrollerAlpha());
+    }
+
+    /**
+     * Sets the list of apps in this view, used to determine the fastscroll position.
+     */
+    public void setApps(AlphabeticalAppsList apps) {
+        mApps = apps;
+    }
+
+    /**
+     * Sets the number of apps per row in this recycler view.
+     */
+    public void setNumAppsPerRow(int rowSize) {
+        mNumAppsPerRow = rowSize;
+    }
+
+    /**
+     * Sets the fast scroller alpha.
+     */
+    public void setFastScrollerAlpha(float alpha) {
+        mFastScrollAlpha = alpha;
+        invalidateFastScroller();
+    }
+
+    /**
+     * Gets the fast scroller alpha.
+     */
+    public float getFastScrollerAlpha() {
+        return mFastScrollAlpha;
+    }
+
+    /**
+     * Returns the scroll bar width.
+     */
+    public int getScrollbarWidth() {
+        return mScrollbarWidth;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        addOnItemTouchListener(this);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+        drawVerticalScrubber(canvas);
+        drawFastScrollerPopup(canvas);
+    }
+
+    /**
+     * We intercept the touch handling only to support fast scrolling when initiated from the
+     * scroll bar.  Otherwise, we fall back to the default RecyclerView touch handling.
+     */
+    @Override
+    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
+        return handleTouchEvent(ev);
+    }
+
+    @Override
+    public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
+        handleTouchEvent(ev);
+    }
+
+    /**
+     * Handles the touch event and determines whether to show the fast scroller (or updates it if
+     * it is already showing).
+     */
+    private boolean handleTouchEvent(MotionEvent ev) {
+        ViewConfiguration config = ViewConfiguration.get(getContext());
+
+        int action = ev.getAction();
+        int x = (int) ev.getX();
+        int y = (int) ev.getY();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                // Keep track of the down positions
+                mDownX = mLastX = x;
+                mDownY = mLastY = y;
+                stopScroll();
+                break;
+            case MotionEvent.ACTION_MOVE:
+                // Check if we are scrolling
+                if (!mDraggingFastScroller && isPointNearScrollbar(mDownX, mDownY) &&
+                        Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                    mDraggingFastScroller = true;
+                    animateFastScrollerVisibility(true);
+                }
+                if (mDraggingFastScroller) {
+                    mLastX = x;
+                    mLastY = y;
+
+                    // Scroll to the right position, and update the section name
+                    int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
+                    int bottom = getHeight() - getPaddingBottom() -
+                            (mFastScrollerBg.getBounds().height() / 2);
+                    float boundedY = (float) Math.max(top, Math.min(bottom, y));
+                    mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
+                            (bottom - top));
+                    invalidateFastScroller();
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mDraggingFastScroller = false;
+                animateFastScrollerVisibility(false);
+                break;
+        }
+        return mDraggingFastScroller;
+    }
+
+    /**
+     * Animates the visibility of the fast scroller popup.
+     */
+    private void animateFastScrollerVisibility(boolean visible) {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
+        anim.setDuration(visible ? 200 : 150);
+        anim.start();
+    }
+
+    /**
+     * Returns whether a given point is near the scrollbar.
+     */
+    private boolean isPointNearScrollbar(int x, int y) {
+        // Check if we are scrolling
+        updateVerticalScrollbarBounds();
+        mVerticalScrollbarBounds.inset(mScrollbarInset, mScrollbarInset);
+        return mVerticalScrollbarBounds.contains(x, y);
+    }
+
+    /**
+     * Draws the fast scroller popup.
+     */
+    private void drawFastScrollerPopup(Canvas canvas) {
+        if (mFastScrollAlpha > 0f) {
+            int x;
+            int y;
+            boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                    LAYOUT_DIRECTION_RTL);
+
+            // Calculate the position for the fast scroller popup
+            Rect bgBounds = mFastScrollerBg.getBounds();
+            if (isRtl) {
+                x = getPaddingLeft() + getScrollBarSize();
+            } else {
+                x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
+            }
+            y = mLastY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgBounds.height());
+            y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
+                    bgBounds.height()));
+
+            // Draw the fast scroller popup
+            int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+            canvas.translate(x, y);
+            mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
+            mFastScrollerBg.draw(canvas);
+            mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
+            mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
+                    mFastScrollSectionName.length(), mFastScrollTextBounds);
+            canvas.drawText(mFastScrollSectionName,
+                    (bgBounds.width() - mFastScrollTextBounds.width()) / 2,
+                    bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2,
+                    mFastScrollTextPaint);
+            canvas.restoreToCount(restoreCount);
+        }
+    }
+
+    /**
+     * Draws the vertical scrollbar.
+     */
+    private void drawVerticalScrubber(Canvas canvas) {
+        updateVerticalScrollbarBounds();
+
+        // Draw the scroll bar
+        int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.translate(mVerticalScrollbarBounds.left, mVerticalScrollbarBounds.top);
+        mScrollbar.setBounds(0, 0, mScrollbarWidth, mVerticalScrollbarBounds.height());
+        mScrollbar.draw(canvas);
+        canvas.restoreToCount(restoreCount);
+    }
+
+    /**
+     * Invalidates the fast scroller popup.
+     */
+    private void invalidateFastScroller() {
+        invalidate(getWidth() - getPaddingRight() - getScrollBarSize() -
+                mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight());
+    }
+
+    /**
+     * Maps the progress (from 0..1) to the position that should be visible
+     */
+    private String scrollToPositionAtProgress(float progress) {
+        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+        if (sections.isEmpty()) {
+            return "";
+        }
+
+        // Find the position of the first application in the section that contains the row at the
+        // current progress
+        int rowAtProgress = (int) (progress * getNumRows());
+        int rowCount = 0;
+        AlphabeticalAppsList.SectionInfo lastSectionInfo = null;
+        for (AlphabeticalAppsList.SectionInfo section : sections) {
+            int numRowsInSection = (int) Math.ceil((float) section.numAppsInSection / mNumAppsPerRow);
+            if (rowCount + numRowsInSection >= rowAtProgress) {
+                lastSectionInfo = section;
+                break;
+            }
+            rowCount += numRowsInSection;
+        }
+        int position = mApps.getAdapterItems().indexOf(lastSectionInfo.firstAppItem);
+
+        // Scroll the position into view, anchored at the top of the screen if possible. We call the
+        // scroll method on the LayoutManager directly since it is not exposed by RecyclerView.
+        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+        stopScroll();
+        layoutManager.scrollToPositionWithOffset(position, 0);
+
+        // Return the section name of the row
+        return mApps.getAdapterItems().get(position).sectionName;
+    }
+
+    /**
+     * Returns the bounds for the scrollbar.
+     */
+    private void updateVerticalScrollbarBounds() {
+        // Skip early if there are no items
+        if (mApps.getAdapterItems().isEmpty()) {
+            mVerticalScrollbarBounds.setEmpty();
+            return;
+        }
+
+        // Find the index and height of the first visible row (all rows have the same height)
+        int x;
+        int y;
+        boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                LAYOUT_DIRECTION_RTL);
+        int rowIndex = -1;
+        int rowTopOffset = -1;
+        int rowHeight = -1;
+        int rowCount = getNumRows();
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            int position = getChildPosition(child);
+            if (position != NO_POSITION) {
+                AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+                if (!item.isSectionHeader) {
+                    rowIndex = findRowForAppIndex(item.appIndex);
+                    rowTopOffset = getLayoutManager().getDecoratedTop(child);
+                    rowHeight = child.getHeight();
+                    break;
+                }
+            }
+        }
+
+        if (rowIndex != -1) {
+            int height = getHeight() - getPaddingTop() - getPaddingBottom();
+            int totalScrollHeight = rowCount * rowHeight;
+            if (totalScrollHeight > height) {
+                int scrollbarHeight = Math.max(mScrollbarMinHeight,
+                        (int) (height / ((float) totalScrollHeight / height)));
+
+                // Calculate the position and size of the scroll bar
+                if (isRtl) {
+                    x = getPaddingLeft();
+                } else {
+                    x = getWidth() - getPaddingRight() - mScrollbarWidth;
+                }
+
+                // To calculate the offset, we compute the percentage of the total scrollable height
+                // that the user has already scrolled and then map that to the scroll bar bounds
+                int availableY = totalScrollHeight - height;
+                int availableScrollY = height - scrollbarHeight;
+                y = (rowIndex * rowHeight) - rowTopOffset;
+                y = getPaddingTop() +
+                        (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
+
+                mVerticalScrollbarBounds.set(x, y, x + mScrollbarWidth, y + scrollbarHeight);
+                return;
+            }
+        }
+        mVerticalScrollbarBounds.setEmpty();
+    }
+
+    /**
+     * Returns the row index for a given position in the list.
+     */
+    private int findRowForAppIndex(int position) {
+        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+        int appIndex = 0;
+        int rowCount = 0;
+        for (AlphabeticalAppsList.SectionInfo info : sections) {
+            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
+            if (appIndex + info.numAppsInSection > position) {
+                return rowCount + ((position - appIndex) / mNumAppsPerRow);
+            }
+            appIndex += info.numAppsInSection;
+            rowCount += numRowsInSection;
+        }
+        return appIndex;
+    }
+
+    /**
+     * Returns the total number of rows in the list.
+     */
+    private int getNumRows() {
+        List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
+        int rowCount = 0;
+        for (AlphabeticalAppsList.SectionInfo info : sections) {
+            int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
+            rowCount += numRowsInSection;
+        }
+        return rowCount;
+    }
+}
diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java
new file mode 100644
index 0000000..b247145
--- /dev/null
+++ b/src/com/android/launcher3/AppsContainerView.java
@@ -0,0 +1,549 @@
+/*
+ * 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.launcher3;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.InsetDrawable;
+import android.support.v7.widget.RecyclerView;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.util.Thunk;
+
+import java.util.List;
+
+
+/**
+ * The all apps list view container.
+ */
+public class AppsContainerView extends FrameLayout implements DragSource, Insettable, TextWatcher,
+        TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener,
+        View.OnLongClickListener {
+
+    private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
+
+    private static final int GRID_LAYOUT = 0;
+    private static final int LIST_LAYOUT = 1;
+    private static final int USE_LAYOUT = GRID_LAYOUT;
+
+    @Thunk Launcher mLauncher;
+    @Thunk AlphabeticalAppsList mApps;
+    private RecyclerView.Adapter mAdapter;
+    private RecyclerView.LayoutManager mLayoutManager;
+    private RecyclerView.ItemDecoration mItemDecoration;
+
+    private LinearLayout mContentView;
+    @Thunk AppsContainerRecyclerView mAppsRecyclerView;
+    private EditText mSearchBarView;
+    
+    private int mNumAppsPerRow;
+    private Point mLastTouchDownPos = new Point(-1, -1);
+    private Point mLastTouchPos = new Point();
+    private Rect mInsets = new Rect();
+    private Rect mFixedBounds = new Rect();
+    private int mContentMarginStart;
+    // Normal container insets
+    private int mContainerInset;
+    // Fixed bounds container insets
+    private int mFixedBoundsContainerInset;
+
+    public AppsContainerView(Context context) {
+        this(context, null);
+    }
+
+    public AppsContainerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+        Resources res = context.getResources();
+
+        mContainerInset = context.getResources().getDimensionPixelSize(
+                R.dimen.apps_container_inset);
+        mFixedBoundsContainerInset = context.getResources().getDimensionPixelSize(
+                R.dimen.apps_container_fixed_bounds_inset);
+        mLauncher = (Launcher) context;
+        mApps = new AlphabeticalAppsList(context);
+        if (USE_LAYOUT == GRID_LAYOUT) {
+            mNumAppsPerRow = grid.appsViewNumCols;
+            AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this,
+                    mLauncher, this);
+            adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
+            mLayoutManager = adapter.getLayoutManager(context);
+            mItemDecoration = adapter.getItemDecoration();
+            mAdapter = adapter;
+            mContentMarginStart = adapter.getContentMarginStart();
+        } else if (USE_LAYOUT == LIST_LAYOUT) {
+            mNumAppsPerRow = 1;
+            AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this);
+            adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
+            mLayoutManager = adapter.getLayoutManager(context);
+            mAdapter = adapter;
+        }
+        mApps.setAdapter(mAdapter);
+    }
+
+    /**
+     * Sets the current set of apps.
+     */
+    public void setApps(List<AppInfo> apps) {
+        mApps.setApps(apps);
+    }
+
+    /**
+     * Adds new apps to the list.
+     */
+    public void addApps(List<AppInfo> apps) {
+        mApps.addApps(apps);
+    }
+
+    /**
+     * Updates existing apps in the list
+     */
+    public void updateApps(List<AppInfo> apps) {
+        mApps.updateApps(apps);
+    }
+
+    /**
+     * Removes some apps from the list.
+     */
+    public void removeApps(List<AppInfo> apps) {
+        mApps.removeApps(apps);
+    }
+
+    /**
+     * Hides the search bar
+     */
+    public void hideSearchBar() {
+        mSearchBarView.setVisibility(View.GONE);
+        updateBackgrounds();
+        updatePaddings();
+    }
+
+    /**
+     * Scrolls this list view to the top.
+     */
+    public void scrollToTop() {
+        mAppsRecyclerView.scrollToPosition(0);
+    }
+
+    /**
+     * Returns the content view used for the launcher transitions.
+     */
+    public View getContentView() {
+        return mContentView;
+    }
+
+    /**
+     * Returns the reveal view used for the launcher transitions.
+     */
+    public View getRevealView() {
+        return findViewById(R.id.apps_view_transition_overlay);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                LAYOUT_DIRECTION_RTL);
+        if (USE_LAYOUT == GRID_LAYOUT) {
+            ((AppsGridAdapter) mAdapter).setRtl(isRtl);
+        }
+
+        // Work around the search box getting first focus and showing the cursor by
+        // proxying the focus from the content view to the recycler view directly
+        mContentView = (LinearLayout) findViewById(R.id.apps_list);
+        mContentView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+            @Override
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (v == mContentView && hasFocus) {
+                    mAppsRecyclerView.requestFocus();
+                }
+            }
+        });
+        mSearchBarView = (EditText) findViewById(R.id.app_search_box);
+        if (mSearchBarView != null) {
+            mSearchBarView.addTextChangedListener(this);
+            mSearchBarView.setOnEditorActionListener(this);
+        }
+        mAppsRecyclerView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view);
+        mAppsRecyclerView.setApps(mApps);
+        mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
+        mAppsRecyclerView.setLayoutManager(mLayoutManager);
+        mAppsRecyclerView.setAdapter(mAdapter);
+        mAppsRecyclerView.setHasFixedSize(true);
+        if (mItemDecoration != null) {
+            mAppsRecyclerView.addItemDecoration(mItemDecoration);
+        }
+        updateBackgrounds();
+        updatePaddings();
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+        mInsets.set(insets);
+        updatePaddings();
+    }
+
+    /**
+     * Sets the fixed bounds for this Apps view.
+     */
+    public void setFixedBounds(Context context, Rect fixedBounds) {
+        if (!fixedBounds.isEmpty() && !fixedBounds.equals(mFixedBounds)) {
+            // Update the number of items in the grid
+            LauncherAppState app = LauncherAppState.getInstance();
+            DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+            if (grid.updateAppsViewNumCols(context.getResources(), fixedBounds.width())) {
+                mNumAppsPerRow = grid.appsViewNumCols;
+                mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
+                if (USE_LAYOUT == GRID_LAYOUT) {
+                    ((AppsGridAdapter) mAdapter).setNumAppsPerRow(mNumAppsPerRow);
+                }
+            }
+
+            mFixedBounds.set(fixedBounds);
+        }
+        updateBackgrounds();
+        updatePaddings();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return handleTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return handleTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                mLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        // Return early if this is not initiated from a touch
+        if (!v.isInTouchMode()) return false;
+        // When we have exited all apps or are in transition, disregard long clicks
+        if (!mLauncher.isAppsViewVisible() ||
+                mLauncher.getWorkspace().isSwitchingState()) return false;
+        // Return if global dragging is not enabled
+        if (!mLauncher.isDraggingEnabled()) return false;
+
+        // Start the drag
+        mLauncher.getWorkspace().beginDragShared(v, mLastTouchPos, this, false);
+
+        // We delay entering spring-loaded mode slightly to make sure the UI
+        // thready is free of any work.
+        postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                // We don't enter spring-loaded mode if the drag has been cancelled
+                if (mLauncher.getDragController().isDragging()) {
+                    // Go into spring loaded mode (must happen before we startDrag())
+                    mLauncher.enterSpringLoadedDragMode();
+                }
+            }
+        }, 150);
+
+        return false;
+    }
+
+    @Override
+    public boolean supportsFlingToDelete() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsAppInfoDropTarget() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsDeleteDropTarget() {
+        return false;
+    }
+
+    @Override
+    public float getIntrinsicIconScaleFactor() {
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+        return (float) grid.allAppsIconSizePx / grid.iconSizePx;
+    }
+
+    @Override
+    public void onFlingToDeleteCompleted() {
+        // We just dismiss the drag when we fling, so cleanup here
+        mLauncher.exitSpringLoadedDragModeDelayed(true,
+                Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+        mLauncher.unlockScreenOrientation(false);
+    }
+
+    @Override
+    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+            boolean success) {
+        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
+                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
+            // Exit spring loaded mode if we have not successfully dropped or have not handled the
+            // drop in Workspace
+            mLauncher.exitSpringLoadedDragModeDelayed(true,
+                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+        }
+        mLauncher.unlockScreenOrientation(false);
+
+        // Display an error message if the drag failed due to there not being enough space on the
+        // target layout we were dropping on.
+        if (!success) {
+            boolean showOutOfSpaceMessage = false;
+            if (target instanceof Workspace) {
+                int currentScreen = mLauncher.getCurrentWorkspaceScreen();
+                Workspace workspace = (Workspace) target;
+                CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
+                ItemInfo itemInfo = (ItemInfo) d.dragInfo;
+                if (layout != null) {
+                    layout.calculateSpans(itemInfo);
+                    showOutOfSpaceMessage =
+                            !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
+                }
+            }
+            if (showOutOfSpaceMessage) {
+                mLauncher.showOutOfSpaceMessage(false);
+            }
+
+            d.deferDragViewCleanupPostAnimation = false;
+        }
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        // Do nothing
+    }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        // Do nothing
+    }
+
+    @Override
+    public void afterTextChanged(final Editable s) {
+        if (s.toString().isEmpty()) {
+            mApps.setFilter(null);
+        } else {
+            String formatStr = getResources().getString(R.string.apps_view_no_search_results);
+            if (USE_LAYOUT == GRID_LAYOUT) {
+                ((AppsGridAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
+                        s.toString()));
+            } else {
+                ((AppsListAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
+                        s.toString()));
+            }
+
+            final String filterText = s.toString().toLowerCase().replaceAll("\\s+", "");
+            mApps.setFilter(new AlphabeticalAppsList.Filter() {
+                @Override
+                public boolean retainApp(AppInfo info, String sectionName) {
+                    String title = info.title.toString();
+                    return sectionName.toLowerCase().contains(filterText) ||
+                            title.toLowerCase().replaceAll("\\s+", "").contains(filterText);
+                }
+            });
+        }
+    }
+
+    @Override
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
+            // Skip the quick-launch if there isn't exactly one item
+            if (mApps.getSize() != 1) {
+                return false;
+            }
+
+            List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+            for (int i = 0; i < items.size(); i++) {
+                AlphabeticalAppsList.AdapterItem item = items.get(i);
+                if (!item.isSectionHeader) {
+                    mAppsRecyclerView.getChildAt(i).performClick();
+                    InputMethodManager imm = (InputMethodManager)
+                            getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public View getContent() {
+        return null;
+    }
+
+    @Override
+    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
+        // Do nothing
+    }
+
+    @Override
+    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
+        // Do nothing
+    }
+
+    @Override
+    public void onLauncherTransitionStep(Launcher l, float t) {
+        // Do nothing
+    }
+
+    @Override
+    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
+        if (mSearchBarView != null) {
+            if (toWorkspace) {
+                // Clear the search bar
+                mSearchBarView.setText("");
+            }
+        }
+    }
+
+    /**
+     * Handles the touch events to dismiss all apps when clicking outside the bounds of the
+     * recycler view.
+     */
+    private boolean handleTouchEvent(MotionEvent ev) {
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                if (!mFixedBounds.isEmpty()) {
+                    // Outset the fixed bounds and check if the touch is outside all apps
+                    Rect tmpRect = new Rect(mFixedBounds);
+                    tmpRect.inset(-grid.allAppsIconSizePx / 2, 0);
+                    if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) {
+                        mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY());
+                        return true;
+                    }
+                } else {
+                    // Check if the touch is outside all apps
+                    if (ev.getX() < getPaddingLeft() ||
+                            ev.getX() > (getWidth() - getPaddingRight())) {
+                        mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY());
+                        return true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mLastTouchDownPos.x > -1) {
+                    ViewConfiguration viewConfig = ViewConfiguration.get(getContext());
+                    float dx = ev.getX() - mLastTouchDownPos.x;
+                    float dy = ev.getY() - mLastTouchDownPos.y;
+                    float distance = (float) Math.hypot(dx, dy);
+                    if (distance < viewConfig.getScaledTouchSlop()) {
+                        // The background was clicked, so just go home
+                        Launcher launcher = (Launcher) getContext();
+                        launcher.showWorkspace(true);
+                        return true;
+                    }
+                }
+                // Fall through
+            case MotionEvent.ACTION_CANCEL:
+                mLastTouchDownPos.set(-1, -1);
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * Update the padding of the Apps view and children.  To ensure that the RecyclerView has the
+     * full width to handle touches right to the edge of the screen, we only apply the top and
+     * bottom padding to the AppsContainerView and then the left/right padding on the RecyclerView
+     * itself.  In particular, the left/right padding is applied to the background of the view,
+     * and then additionally inset by the start margin.
+     */
+    private void updatePaddings() {
+        boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
+                LAYOUT_DIRECTION_RTL);
+        boolean hasSearchBar = (mSearchBarView != null) &&
+                (mSearchBarView.getVisibility() == View.VISIBLE);
+
+        if (mFixedBounds.isEmpty()) {
+            // If there are no fixed bounds, then use the default padding and insets
+            setPadding(mInsets.left, mContainerInset + mInsets.top, mInsets.right,
+                    mContainerInset + mInsets.bottom);
+        } else {
+            // If there are fixed bounds, then we update the padding to reflect the fixed bounds.
+            setPadding(mFixedBounds.left, mFixedBounds.top + mFixedBoundsContainerInset,
+                    getMeasuredWidth() - mFixedBounds.right,
+                    mInsets.bottom + mFixedBoundsContainerInset);
+        }
+
+        // Update the apps recycler view
+        int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
+        if (isRtl) {
+            mAppsRecyclerView.setPadding(inset, inset, inset + mContentMarginStart, inset);
+        } else {
+            mAppsRecyclerView.setPadding(inset + mContentMarginStart, inset, inset, inset);
+        }
+
+        // Update the search bar
+        if (hasSearchBar) {
+            LinearLayout.LayoutParams lp =
+                    (LinearLayout.LayoutParams) mSearchBarView.getLayoutParams();
+            lp.leftMargin = lp.rightMargin = inset;
+        }
+    }
+
+    /**
+     * Update the background of the Apps view and children.
+     */
+    private void updateBackgrounds() {
+        int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
+        boolean hasSearchBar = (mSearchBarView != null) &&
+                (mSearchBarView.getVisibility() == View.VISIBLE);
+
+        // Update the background of the reveal view and list to be inset with the fixed bound
+        // insets instead of the default insets
+        mAppsRecyclerView.setBackground(new InsetDrawable(
+                getContext().getResources().getDrawable(
+                        hasSearchBar ? R.drawable.apps_list_search_bg : R.drawable.apps_list_bg),
+                inset, 0, inset, 0));
+        getRevealView().setBackground(new InsetDrawable(
+                getContext().getResources().getDrawable(R.drawable.apps_reveal_bg),
+                inset, 0, inset, 0));
+    }
+}
diff --git a/src/com/android/launcher3/AppsCustomizePagedView.java b/src/com/android/launcher3/AppsCustomizePagedView.java
deleted file mode 100644
index 9e7e523..0000000
--- a/src/com/android/launcher3/AppsCustomizePagedView.java
+++ /dev/null
@@ -1,1574 +0,0 @@
-/*
- * Copyright (C) 2011 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.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.appwidget.AppWidgetHostView;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProviderInfo;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Process;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-import android.widget.GridLayout;
-import android.widget.ImageView;
-import android.widget.Toast;
-
-import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * A simple callback interface which also provides the results of the task.
- */
-interface AsyncTaskCallback {
-    void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data);
-}
-
-/**
- * The data needed to perform either of the custom AsyncTasks.
- */
-class AsyncTaskPageData {
-    enum Type {
-        LoadWidgetPreviewData
-    }
-
-    AsyncTaskPageData(int p, ArrayList<Object> l, int cw, int ch, AsyncTaskCallback bgR,
-            AsyncTaskCallback postR, WidgetPreviewLoader w) {
-        page = p;
-        items = l;
-        generatedImages = new ArrayList<Bitmap>();
-        maxImageWidth = cw;
-        maxImageHeight = ch;
-        doInBackgroundCallback = bgR;
-        postExecuteCallback = postR;
-        widgetPreviewLoader = w;
-    }
-    void cleanup(boolean cancelled) {
-        // Clean up any references to source/generated bitmaps
-        if (generatedImages != null) {
-            if (cancelled) {
-                for (int i = 0; i < generatedImages.size(); i++) {
-                    widgetPreviewLoader.recycleBitmap(items.get(i), generatedImages.get(i));
-                }
-            }
-            generatedImages.clear();
-        }
-    }
-    int page;
-    ArrayList<Object> items;
-    ArrayList<Bitmap> sourceImages;
-    ArrayList<Bitmap> generatedImages;
-    int maxImageWidth;
-    int maxImageHeight;
-    AsyncTaskCallback doInBackgroundCallback;
-    AsyncTaskCallback postExecuteCallback;
-    WidgetPreviewLoader widgetPreviewLoader;
-}
-
-/**
- * A generic template for an async task used in AppsCustomize.
- */
-class AppsCustomizeAsyncTask extends AsyncTask<AsyncTaskPageData, Void, AsyncTaskPageData> {
-    AppsCustomizeAsyncTask(int p, AsyncTaskPageData.Type ty) {
-        page = p;
-        threadPriority = Process.THREAD_PRIORITY_DEFAULT;
-        dataType = ty;
-    }
-    @Override
-    protected AsyncTaskPageData doInBackground(AsyncTaskPageData... params) {
-        if (params.length != 1) return null;
-        // Load each of the widget previews in the background
-        params[0].doInBackgroundCallback.run(this, params[0]);
-        return params[0];
-    }
-    @Override
-    protected void onPostExecute(AsyncTaskPageData result) {
-        // All the widget previews are loaded, so we can just callback to inflate the page
-        result.postExecuteCallback.run(this, result);
-    }
-
-    void setThreadPriority(int p) {
-        threadPriority = p;
-    }
-    void syncThreadPriority() {
-        Process.setThreadPriority(threadPriority);
-    }
-
-    // The page that this async task is associated with
-    AsyncTaskPageData.Type dataType;
-    int page;
-    int threadPriority;
-}
-
-/**
- * The Apps/Customize page that displays all the applications, widgets, and shortcuts.
- */
-public class AppsCustomizePagedView extends PagedViewWithDraggableItems implements
-        View.OnClickListener, View.OnKeyListener, DragSource,
-        PagedViewWidget.ShortPressListener, LauncherTransitionable {
-    static final String TAG = "AppsCustomizePagedView";
-
-    private static Rect sTmpRect = new Rect();
-
-    /**
-     * The different content types that this paged view can show.
-     */
-    public enum ContentType {
-        Applications,
-        Widgets
-    }
-    private ContentType mContentType = ContentType.Applications;
-
-    // Refs
-    private Launcher mLauncher;
-    private DragController mDragController;
-    private final LayoutInflater mLayoutInflater;
-    private final PackageManager mPackageManager;
-
-    // Save and Restore
-    private int mSaveInstanceStateItemIndex = -1;
-
-    // Content
-    private ArrayList<AppInfo> mApps;
-    private ArrayList<Object> mWidgets;
-
-    // Caching
-    private IconCache mIconCache;
-
-    // Dimens
-    private int mContentWidth, mContentHeight;
-    private int mWidgetCountX, mWidgetCountY;
-    private PagedViewCellLayout mWidgetSpacingLayout;
-    private int mNumAppsPages;
-    private int mNumWidgetPages;
-    private Rect mAllAppsPadding = new Rect();
-
-    // Previews & outlines
-    ArrayList<AppsCustomizeAsyncTask> mRunningTasks;
-    private static final int sPageSleepDelay = 200;
-
-    private Runnable mInflateWidgetRunnable = null;
-    private Runnable mBindWidgetRunnable = null;
-    static final int WIDGET_NO_CLEANUP_REQUIRED = -1;
-    static final int WIDGET_PRELOAD_PENDING = 0;
-    static final int WIDGET_BOUND = 1;
-    static final int WIDGET_INFLATED = 2;
-    int mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED;
-    int mWidgetLoadingId = -1;
-    PendingAddWidgetInfo mCreateWidgetInfo = null;
-    private boolean mDraggingWidget = false;
-    boolean mPageBackgroundsVisible = true;
-
-    private Toast mWidgetInstructionToast;
-
-    // Deferral of loading widget previews during launcher transitions
-    private boolean mInTransition;
-    private ArrayList<AsyncTaskPageData> mDeferredSyncWidgetPageItems =
-        new ArrayList<AsyncTaskPageData>();
-    private ArrayList<Runnable> mDeferredPrepareLoadWidgetPreviewsTasks =
-        new ArrayList<Runnable>();
-
-    WidgetPreviewLoader mWidgetPreviewLoader;
-
-    private boolean mInBulkBind;
-    private boolean mNeedToUpdatePageCountsAndInvalidateData;
-
-    public AppsCustomizePagedView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        mLayoutInflater = LayoutInflater.from(context);
-        mPackageManager = context.getPackageManager();
-        mApps = new ArrayList<AppInfo>();
-        mWidgets = new ArrayList<Object>();
-        mIconCache = (LauncherAppState.getInstance()).getIconCache();
-        mRunningTasks = new ArrayList<AppsCustomizeAsyncTask>();
-
-        // Save the default widget preview background
-        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppsCustomizePagedView, 0, 0);
-        mWidgetCountX = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountX, 2);
-        mWidgetCountY = a.getInt(R.styleable.AppsCustomizePagedView_widgetCountY, 2);
-        a.recycle();
-        mWidgetSpacingLayout = new PagedViewCellLayout(getContext());
-
-        // The padding on the non-matched dimension for the default widget preview icons
-        // (top + bottom)
-        mFadeInAdjacentScreens = false;
-
-        // Unless otherwise specified this view is important for accessibility.
-        if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
-            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
-        }
-        setSinglePageInViewport();
-    }
-
-    @Override
-    protected void init() {
-        super.init();
-        mCenterPagesVertically = false;
-
-        Context context = getContext();
-        Resources r = context.getResources();
-        setDragSlopeThreshold(r.getInteger(R.integer.config_appsCustomizeDragSlopeThreshold)/100f);
-    }
-
-    public void onFinishInflate() {
-        super.onFinishInflate();
-
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-        setPadding(grid.edgeMarginPx, 2 * grid.edgeMarginPx,
-                grid.edgeMarginPx, 2 * grid.edgeMarginPx);
-    }
-
-    void setAllAppsPadding(Rect r) {
-        mAllAppsPadding.set(r);
-    }
-
-    void setWidgetsPageIndicatorPadding(int pageIndicatorHeight) {
-        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), pageIndicatorHeight);
-    }
-
-    WidgetPreviewLoader getWidgetPreviewLoader() {
-        if (mWidgetPreviewLoader == null) {
-            mWidgetPreviewLoader = new WidgetPreviewLoader(mLauncher);
-        }
-        return mWidgetPreviewLoader;
-    }
-
-    /** Returns the item index of the center item on this page so that we can restore to this
-     *  item index when we rotate. */
-    private int getMiddleComponentIndexOnCurrentPage() {
-        int i = -1;
-        if (getPageCount() > 0) {
-            int currentPage = getCurrentPage();
-            if (mContentType == ContentType.Applications) {
-                AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(currentPage);
-                ShortcutAndWidgetContainer childrenLayout = layout.getShortcutsAndWidgets();
-                int numItemsPerPage = mCellCountX * mCellCountY;
-                int childCount = childrenLayout.getChildCount();
-                if (childCount > 0) {
-                    i = (currentPage * numItemsPerPage) + (childCount / 2);
-                }
-            } else if (mContentType == ContentType.Widgets) {
-                int numApps = mApps.size();
-                PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(currentPage);
-                int numItemsPerPage = mWidgetCountX * mWidgetCountY;
-                int childCount = layout.getChildCount();
-                if (childCount > 0) {
-                    i = numApps +
-                        (currentPage * numItemsPerPage) + (childCount / 2);
-                }
-            } else {
-                throw new RuntimeException("Invalid ContentType");
-            }
-        }
-        return i;
-    }
-
-    /** Get the index of the item to restore to if we need to restore the current page. */
-    int getSaveInstanceStateIndex() {
-        if (mSaveInstanceStateItemIndex == -1) {
-            mSaveInstanceStateItemIndex = getMiddleComponentIndexOnCurrentPage();
-        }
-        return mSaveInstanceStateItemIndex;
-    }
-
-    /** Returns the page in the current orientation which is expected to contain the specified
-     *  item index. */
-    int getPageForComponent(int index) {
-        if (index < 0) return 0;
-
-        if (index < mApps.size()) {
-            int numItemsPerPage = mCellCountX * mCellCountY;
-            return (index / numItemsPerPage);
-        } else {
-            int numItemsPerPage = mWidgetCountX * mWidgetCountY;
-            return (index - mApps.size()) / numItemsPerPage;
-        }
-    }
-
-    /** Restores the page for an item at the specified index */
-    void restorePageForIndex(int index) {
-        if (index < 0) return;
-        mSaveInstanceStateItemIndex = index;
-    }
-
-    private void updatePageCounts() {
-        mNumWidgetPages = (int) Math.ceil(mWidgets.size() /
-                (float) (mWidgetCountX * mWidgetCountY));
-        mNumAppsPages = (int) Math.ceil((float) mApps.size() / (mCellCountX * mCellCountY));
-    }
-
-    protected void onDataReady(int width, int height) {
-        // Now that the data is ready, we can calculate the content width, the number of cells to
-        // use for each page
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-        mCellCountX = (int) grid.allAppsNumCols;
-        mCellCountY = (int) grid.allAppsNumRows;
-        updatePageCounts();
-
-        // Force a measure to update recalculate the gaps
-        mContentWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
-        mContentHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
-        int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST);
-        int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST);
-        mWidgetSpacingLayout.measure(widthSpec, heightSpec);
-
-        final boolean hostIsTransitioning = getTabHost().isInTransition();
-        int page = getPageForComponent(mSaveInstanceStateItemIndex);
-        invalidatePageData(Math.max(0, page), hostIsTransitioning);
-    }
-
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-
-        if (!isDataReady()) {
-            if ((LauncherAppState.isDisableAllApps() || !mApps.isEmpty()) && !mWidgets.isEmpty()) {
-                post(new Runnable() {
-                    // This code triggers requestLayout so must be posted outside of the
-                    // layout pass.
-                    public void run() {
-                        if (Utilities.isViewAttachedToWindow(AppsCustomizePagedView.this)) {
-                            setDataIsReady();
-                            onDataReady(getMeasuredWidth(), getMeasuredHeight());
-                        }
-                    }
-                });
-            }
-        }
-    }
-
-    public void onPackagesUpdated(ArrayList<Object> widgetsAndShortcuts) {
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-
-        // Get the list of widgets and shortcuts
-        mWidgets.clear();
-        for (Object o : widgetsAndShortcuts) {
-            if (o instanceof LauncherAppWidgetProviderInfo) {
-                LauncherAppWidgetProviderInfo widget = (LauncherAppWidgetProviderInfo) o;
-                if (!app.shouldShowAppOrWidgetProvider(widget.provider) && !widget.isCustomWidget) {
-                    continue;
-                }
-
-                if (widget.minSpanX > 0 && widget.minSpanY > 0) {
-                    // Ensure that all widgets we show can be added on a workspace of this size
-                    int[] spanXY = Launcher.getSpanForWidget(mLauncher, widget);
-                    int[] minSpanXY = Launcher.getMinSpanForWidget(mLauncher, widget);
-                    int minSpanX = Math.min(spanXY[0], minSpanXY[0]);
-                    int minSpanY = Math.min(spanXY[1], minSpanXY[1]);
-                    if (minSpanX <= (int) grid.numColumns &&
-                        minSpanY <= (int) grid.numRows) {
-                        mWidgets.add(widget);
-                    } else {
-                        Log.e(TAG, "Widget " + widget.provider + " can not fit on this device (" +
-                              widget.minWidth + ", " + widget.minHeight + ")");
-                    }
-                } else {
-                    Log.e(TAG, "Widget " + widget.provider + " has invalid dimensions (" +
-                          widget.minWidth + ", " + widget.minHeight + ")");
-                }
-            } else {
-                // just add shortcuts
-                mWidgets.add(o);
-            }
-        }
-
-        updatePageCountsAndInvalidateData();
-    }
-
-    public void setBulkBind(boolean bulkBind) {
-        if (bulkBind) {
-            mInBulkBind = true;
-        } else {
-            mInBulkBind = false;
-            if (mNeedToUpdatePageCountsAndInvalidateData) {
-                updatePageCountsAndInvalidateData();
-            }
-        }
-    }
-
-    private void updatePageCountsAndInvalidateData() {
-        if (mInBulkBind) {
-            mNeedToUpdatePageCountsAndInvalidateData = true;
-        } else {
-            updatePageCounts();
-            invalidateOnDataChange();
-            mNeedToUpdatePageCountsAndInvalidateData = false;
-        }
-    }
-
-    @Override
-    public void onClick(View v) {
-        // When we have exited all apps or are in transition, disregard clicks
-        if (!mLauncher.isAllAppsVisible()
-                || mLauncher.getWorkspace().isSwitchingState()
-                || !(v instanceof PagedViewWidget)) return;
-
-        // Let the user know that they have to long press to add a widget
-        if (mWidgetInstructionToast != null) {
-            mWidgetInstructionToast.cancel();
-        }
-        mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add,
-            Toast.LENGTH_SHORT);
-        mWidgetInstructionToast.show();
-
-        // Create a little animation to show that the widget can move
-        float offsetY = getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY);
-        final ImageView p = (ImageView) v.findViewById(R.id.widget_preview);
-        AnimatorSet bounce = LauncherAnimUtils.createAnimatorSet();
-        ValueAnimator tyuAnim = LauncherAnimUtils.ofFloat(p, "translationY", offsetY);
-        tyuAnim.setDuration(125);
-        ValueAnimator tydAnim = LauncherAnimUtils.ofFloat(p, "translationY", 0f);
-        tydAnim.setDuration(100);
-        bounce.play(tyuAnim).before(tydAnim);
-        bounce.setInterpolator(new AccelerateInterpolator());
-        bounce.start();
-    }
-
-    public boolean onKey(View v, int keyCode, KeyEvent event) {
-        return FocusHelper.handleAppsCustomizeKeyEvent(v,  keyCode, event);
-    }
-
-    /*
-     * PagedViewWithDraggableItems implementation
-     */
-    @Override
-    protected void determineDraggingStart(android.view.MotionEvent ev) {
-        // Disable dragging by pulling an app down for now.
-    }
-
-    private void beginDraggingApplication(View v) {
-        mLauncher.getWorkspace().beginDragShared(v, this);
-    }
-
-    static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) {
-        Bundle options = null;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
-            AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, sTmpRect);
-            Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher,
-                    info.componentName, null);
-
-            float density = launcher.getResources().getDisplayMetrics().density;
-            int xPaddingDips = (int) ((padding.left + padding.right) / density);
-            int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
-
-            options = new Bundle();
-            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH,
-                    sTmpRect.left - xPaddingDips);
-            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,
-                    sTmpRect.top - yPaddingDips);
-            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH,
-                    sTmpRect.right - xPaddingDips);
-            options.putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,
-                    sTmpRect.bottom - yPaddingDips);
-        }
-        return options;
-    }
-
-    private void preloadWidget(final PendingAddWidgetInfo info) {
-        final LauncherAppWidgetProviderInfo pInfo = info.info;
-        final Bundle options = pInfo.isCustomWidget ? null :
-                getDefaultOptionsForWidget(mLauncher, info);
-
-        if (pInfo.configure != null) {
-            info.bindOptions = options;
-            return;
-        }
-
-        mWidgetCleanupState = WIDGET_PRELOAD_PENDING;
-        mBindWidgetRunnable = new Runnable() {
-            @Override
-            public void run() {
-                if (pInfo.isCustomWidget) {
-                    mWidgetCleanupState = WIDGET_BOUND;
-                    return;
-                }
-
-                mWidgetLoadingId = mLauncher.getAppWidgetHost().allocateAppWidgetId();
-                if(AppWidgetManagerCompat.getInstance(mLauncher).bindAppWidgetIdIfAllowed(
-                        mWidgetLoadingId, pInfo, options)) {
-                    mWidgetCleanupState = WIDGET_BOUND;
-                }
-
-            }
-        };
-        post(mBindWidgetRunnable);
-
-        mInflateWidgetRunnable = new Runnable() {
-            @Override
-            public void run() {
-                if (mWidgetCleanupState != WIDGET_BOUND) {
-                    return;
-                }
-                AppWidgetHostView hostView = mLauncher.getAppWidgetHost().createView(
-                        getContext(), mWidgetLoadingId, pInfo);
-                info.boundWidget = hostView;
-                mWidgetCleanupState = WIDGET_INFLATED;
-                hostView.setVisibility(INVISIBLE);
-                int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(info.spanX,
-                        info.spanY, info, false);
-
-                // We want the first widget layout to be the correct size. This will be important
-                // for width size reporting to the AppWidgetManager.
-                DragLayer.LayoutParams lp = new DragLayer.LayoutParams(unScaledSize[0],
-                        unScaledSize[1]);
-                lp.x = lp.y = 0;
-                lp.customPosition = true;
-                hostView.setLayoutParams(lp);
-                mLauncher.getDragLayer().addView(hostView);
-            }
-        };
-        post(mInflateWidgetRunnable);
-    }
-
-    @Override
-    public void onShortPress(View v) {
-        // We are anticipating a long press, and we use this time to load bind and instantiate
-        // the widget. This will need to be cleaned up if it turns out no long press occurs.
-        if (mCreateWidgetInfo != null) {
-            // Just in case the cleanup process wasn't properly executed. This shouldn't happen.
-            cleanupWidgetPreloading(false);
-        }
-        mCreateWidgetInfo = new PendingAddWidgetInfo((PendingAddWidgetInfo) v.getTag());
-        preloadWidget(mCreateWidgetInfo);
-    }
-
-    private void cleanupWidgetPreloading(boolean widgetWasAdded) {
-        if (!widgetWasAdded) {
-            // If the widget was not added, we may need to do further cleanup.
-            PendingAddWidgetInfo info = mCreateWidgetInfo;
-            mCreateWidgetInfo = null;
-
-            if (mWidgetCleanupState == WIDGET_PRELOAD_PENDING) {
-                // We never did any preloading, so just remove pending callbacks to do so
-                removeCallbacks(mBindWidgetRunnable);
-                removeCallbacks(mInflateWidgetRunnable);
-            } else if (mWidgetCleanupState == WIDGET_BOUND) {
-                 // Delete the widget id which was allocated
-                if (mWidgetLoadingId != -1 && !info.isCustomWidget()) {
-                    mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId);
-                }
-
-                // We never got around to inflating the widget, so remove the callback to do so.
-                removeCallbacks(mInflateWidgetRunnable);
-            } else if (mWidgetCleanupState == WIDGET_INFLATED) {
-                // Delete the widget id which was allocated
-                if (mWidgetLoadingId != -1 && !info.isCustomWidget()) {
-                    mLauncher.getAppWidgetHost().deleteAppWidgetId(mWidgetLoadingId);
-                }
-
-                // The widget was inflated and added to the DragLayer -- remove it.
-                AppWidgetHostView widget = info.boundWidget;
-                mLauncher.getDragLayer().removeView(widget);
-            }
-        }
-        mWidgetCleanupState = WIDGET_NO_CLEANUP_REQUIRED;
-        mWidgetLoadingId = -1;
-        mCreateWidgetInfo = null;
-        PagedViewWidget.resetShortPressTarget();
-    }
-
-    @Override
-    public void cleanUpShortPress(View v) {
-        if (!mDraggingWidget) {
-            cleanupWidgetPreloading(false);
-        }
-    }
-
-    private boolean beginDraggingWidget(View v) {
-        mDraggingWidget = true;
-        // Get the widget preview as the drag representation
-        ImageView image = (ImageView) v.findViewById(R.id.widget_preview);
-        PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag();
-
-        // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
-        // we abort the drag.
-        if (image.getDrawable() == null) {
-            mDraggingWidget = false;
-            return false;
-        }
-
-        // Compose the drag image
-        Bitmap preview;
-        Bitmap outline;
-        float scale = 1f;
-        Point previewPadding = null;
-
-        if (createItemInfo instanceof PendingAddWidgetInfo) {
-            // This can happen in some weird cases involving multi-touch. We can't start dragging
-            // the widget if this is null, so we break out.
-            if (mCreateWidgetInfo == null) {
-                return false;
-            }
-
-            PendingAddWidgetInfo createWidgetInfo = mCreateWidgetInfo;
-            createItemInfo = createWidgetInfo;
-            int spanX = createItemInfo.spanX;
-            int spanY = createItemInfo.spanY;
-            int[] size = mLauncher.getWorkspace().estimateItemSize(spanX, spanY,
-                    createWidgetInfo, true);
-
-            FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable();
-            float minScale = 1.25f;
-            int maxWidth, maxHeight;
-            maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]);
-            maxHeight = Math.min((int) (previewDrawable.getIntrinsicHeight() * minScale), size[1]);
-
-            int[] previewSizeBeforeScale = new int[1];
-            preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info,
-                    spanX, spanY, maxWidth, maxHeight, null, previewSizeBeforeScale);
-
-            // Compare the size of the drag preview to the preview in the AppsCustomize tray
-            int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0],
-                    getWidgetPreviewLoader().maxWidthForWidgetPreview(spanX));
-            scale = previewWidthInAppsCustomize / (float) preview.getWidth();
-
-            // The bitmap in the AppsCustomize tray is always the the same size, so there
-            // might be extra pixels around the preview itself - this accounts for that
-            if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) {
-                int padding =
-                        (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2;
-                previewPadding = new Point(padding, 0);
-            }
-        } else {
-            PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag();
-            Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.shortcutActivityInfo);
-            preview = Utilities.createIconBitmap(icon, mLauncher);
-            createItemInfo.spanX = createItemInfo.spanY = 1;
-        }
-
-        // Don't clip alpha values for the drag outline if we're using the default widget preview
-        boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo &&
-                (((PendingAddWidgetInfo) createItemInfo).previewImage == 0));
-
-        // Save the preview for the outline generation, then dim the preview
-        outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(),
-                false);
-
-        // Start the drag
-        mLauncher.lockScreenOrientation();
-        mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha);
-        mDragController.startDrag(image, preview, this, createItemInfo,
-                DragController.DRAG_ACTION_COPY, previewPadding, scale);
-        outline.recycle();
-        preview.recycle();
-        return true;
-    }
-
-    @Override
-    protected boolean beginDragging(final View v) {
-        if (!super.beginDragging(v)) return false;
-
-        if (v instanceof BubbleTextView) {
-            beginDraggingApplication(v);
-        } else if (v instanceof PagedViewWidget) {
-            if (!beginDraggingWidget(v)) {
-                return false;
-            }
-        }
-
-        // We delay entering spring-loaded mode slightly to make sure the UI
-        // thready is free of any work.
-        postDelayed(new Runnable() {
-            @Override
-            public void run() {
-                // We don't enter spring-loaded mode if the drag has been cancelled
-                if (mLauncher.getDragController().isDragging()) {
-                    // Go into spring loaded mode (must happen before we startDrag())
-                    mLauncher.enterSpringLoadedDragMode();
-                }
-            }
-        }, 150);
-
-        return true;
-    }
-
-    /**
-     * Clean up after dragging.
-     *
-     * @param target where the item was dragged to (can be null if the item was flung)
-     */
-    private void endDragging(View target, boolean isFlingToDelete, boolean success) {
-        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
-                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
-            // Exit spring loaded mode if we have not successfully dropped or have not handled the
-            // drop in Workspace
-            mLauncher.exitSpringLoadedDragModeDelayed(true,
-                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
-            mLauncher.unlockScreenOrientation(false);
-        } else {
-            mLauncher.unlockScreenOrientation(false);
-        }
-    }
-
-    @Override
-    public View getContent() {
-        if (getChildCount() > 0) {
-            return getChildAt(0);
-        }
-        return null;
-    }
-
-    @Override
-    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
-        mInTransition = true;
-        if (toWorkspace) {
-            cancelAllTasks();
-        }
-    }
-
-    @Override
-    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
-    }
-
-    @Override
-    public void onLauncherTransitionStep(Launcher l, float t) {
-    }
-
-    @Override
-    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
-        mInTransition = false;
-        for (AsyncTaskPageData d : mDeferredSyncWidgetPageItems) {
-            onSyncWidgetPageItems(d, false);
-        }
-        mDeferredSyncWidgetPageItems.clear();
-        for (Runnable r : mDeferredPrepareLoadWidgetPreviewsTasks) {
-            r.run();
-        }
-        mDeferredPrepareLoadWidgetPreviewsTasks.clear();
-        mForceDrawAllChildrenNextFrame = !toWorkspace;
-    }
-
-    @Override
-    public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete,
-            boolean success) {
-        // Return early and wait for onFlingToDeleteCompleted if this was the result of a fling
-        if (isFlingToDelete) return;
-
-        endDragging(target, false, success);
-
-        // Display an error message if the drag failed due to there not being enough space on the
-        // target layout we were dropping on.
-        if (!success) {
-            boolean showOutOfSpaceMessage = false;
-            if (target instanceof Workspace) {
-                int currentScreen = mLauncher.getCurrentWorkspaceScreen();
-                Workspace workspace = (Workspace) target;
-                CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
-                ItemInfo itemInfo = (ItemInfo) d.dragInfo;
-                if (layout != null) {
-                    layout.calculateSpans(itemInfo);
-                    showOutOfSpaceMessage =
-                            !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
-                }
-            }
-            if (showOutOfSpaceMessage) {
-                mLauncher.showOutOfSpaceMessage(false);
-            }
-
-            d.deferDragViewCleanupPostAnimation = false;
-        }
-        cleanupWidgetPreloading(success);
-        mDraggingWidget = false;
-    }
-
-    @Override
-    public void onFlingToDeleteCompleted() {
-        // We just dismiss the drag when we fling, so cleanup here
-        endDragging(null, true, true);
-        cleanupWidgetPreloading(false);
-        mDraggingWidget = false;
-    }
-
-    @Override
-    public boolean supportsFlingToDelete() {
-        return true;
-    }
-
-    @Override
-    public boolean supportsAppInfoDropTarget() {
-        return true;
-    }
-
-    @Override
-    public boolean supportsDeleteDropTarget() {
-        return false;
-    }
-
-    @Override
-    public float getIntrinsicIconScaleFactor() {
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-        return (float) grid.allAppsIconSizePx / grid.iconSizePx;
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        cancelAllTasks();
-    }
-
-    @Override
-    public void trimMemory() {
-        super.trimMemory();
-        clearAllWidgetPages();
-    }
-
-    public void clearAllWidgetPages() {
-        cancelAllTasks();
-        int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            View v = getPageAt(i);
-            if (v instanceof PagedViewGridLayout) {
-                ((PagedViewGridLayout) v).removeAllViewsOnPage();
-                mDirtyPageContent.set(i, true);
-            }
-        }
-    }
-
-    private void cancelAllTasks() {
-        // Clean up all the async tasks
-        Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator();
-        while (iter.hasNext()) {
-            AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next();
-            task.cancel(false);
-            iter.remove();
-            mDirtyPageContent.set(task.page, true);
-
-            // We've already preallocated the views for the data to load into, so clear them as well
-            View v = getPageAt(task.page);
-            if (v instanceof PagedViewGridLayout) {
-                ((PagedViewGridLayout) v).removeAllViewsOnPage();
-            }
-        }
-        mDeferredSyncWidgetPageItems.clear();
-        mDeferredPrepareLoadWidgetPreviewsTasks.clear();
-    }
-
-    public void setContentType(ContentType type) {
-        // Widgets appear to be cleared every time you leave, always force invalidate for them
-        if (mContentType != type || type == ContentType.Widgets) {
-            int page = (mContentType != type) ? 0 : getCurrentPage();
-            mContentType = type;
-            invalidatePageData(page, true);
-        }
-    }
-
-    public ContentType getContentType() {
-        return mContentType;
-    }
-
-    protected void snapToPage(int whichPage, int delta, int duration) {
-        super.snapToPage(whichPage, delta, duration);
-
-        // Update the thread priorities given the direction lookahead
-        Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator();
-        while (iter.hasNext()) {
-            AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next();
-            int pageIndex = task.page;
-            if ((mNextPage > mCurrentPage && pageIndex >= mCurrentPage) ||
-                (mNextPage < mCurrentPage && pageIndex <= mCurrentPage)) {
-                task.setThreadPriority(getThreadPriorityForPage(pageIndex));
-            } else {
-                task.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
-            }
-        }
-    }
-
-    /*
-     * Apps PagedView implementation
-     */
-    private void setVisibilityOnChildren(ViewGroup layout, int visibility) {
-        int childCount = layout.getChildCount();
-        for (int i = 0; i < childCount; ++i) {
-            layout.getChildAt(i).setVisibility(visibility);
-        }
-    }
-    private void setupPage(AppsCustomizeCellLayout layout) {
-        layout.setGridSize(mCellCountX, mCellCountY);
-
-        // Note: We force a measure here to get around the fact that when we do layout calculations
-        // immediately after syncing, we don't have a proper width.  That said, we already know the
-        // expected page width, so we can actually optimize by hiding all the TextView-based
-        // children that are expensive to measure, and let that happen naturally later.
-        setVisibilityOnChildren(layout, View.GONE);
-        int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST);
-        int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST);
-        layout.measure(widthSpec, heightSpec);
-
-        Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel);
-        if (bg != null) {
-            bg.setAlpha(mPageBackgroundsVisible ? 255: 0);
-            layout.setBackground(bg);
-        }
-
-        setVisibilityOnChildren(layout, View.VISIBLE);
-    }
-
-    public void setPageBackgroundsVisible(boolean visible) {
-        mPageBackgroundsVisible = visible;
-        int childCount = getChildCount();
-        for (int i = 0; i < childCount; ++i) {
-            Drawable bg = getChildAt(i).getBackground();
-            if (bg != null) {
-                bg.setAlpha(visible ? 255 : 0);
-            }
-        }
-    }
-
-    public void syncAppsPageItems(int page, boolean immediate) {
-        // ensure that we have the right number of items on the pages
-        final boolean isRtl = isLayoutRtl();
-        int numCells = mCellCountX * mCellCountY;
-        int startIndex = page * numCells;
-        int endIndex = Math.min(startIndex + numCells, mApps.size());
-        AppsCustomizeCellLayout layout = (AppsCustomizeCellLayout) getPageAt(page);
-
-        layout.removeAllViewsOnPage();
-        ArrayList<Object> items = new ArrayList<Object>();
-        ArrayList<Bitmap> images = new ArrayList<Bitmap>();
-        for (int i = startIndex; i < endIndex; ++i) {
-            AppInfo info = mApps.get(i);
-            BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
-                    R.layout.apps_customize_application, layout, false);
-            icon.applyFromApplicationInfo(info);
-            icon.setOnClickListener(mLauncher);
-            icon.setOnLongClickListener(this);
-            icon.setOnTouchListener(this);
-            icon.setOnKeyListener(this);
-            icon.setOnFocusChangeListener(layout.mFocusHandlerView);
-
-            int index = i - startIndex;
-            int x = index % mCellCountX;
-            int y = index / mCellCountX;
-            if (isRtl) {
-                x = mCellCountX - x - 1;
-            }
-            layout.addViewToCellLayout(icon, -1, i, new CellLayout.LayoutParams(x,y, 1,1), false);
-
-            items.add(info);
-            images.add(info.iconBitmap);
-        }
-
-        enableHwLayersOnVisiblePages();
-    }
-
-    /**
-     * A helper to return the priority for loading of the specified widget page.
-     */
-    private int getWidgetPageLoadPriority(int page) {
-        // If we are snapping to another page, use that index as the target page index
-        int toPage = mCurrentPage;
-        if (mNextPage > -1) {
-            toPage = mNextPage;
-        }
-
-        // We use the distance from the target page as an initial guess of priority, but if there
-        // are no pages of higher priority than the page specified, then bump up the priority of
-        // the specified page.
-        Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator();
-        int minPageDiff = Integer.MAX_VALUE;
-        while (iter.hasNext()) {
-            AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next();
-            minPageDiff = Math.abs(task.page - toPage);
-        }
-
-        int rawPageDiff = Math.abs(page - toPage);
-        return rawPageDiff - Math.min(rawPageDiff, minPageDiff);
-    }
-    /**
-     * Return the appropriate thread priority for loading for a given page (we give the current
-     * page much higher priority)
-     */
-    private int getThreadPriorityForPage(int page) {
-        // TODO-APPS_CUSTOMIZE: detect number of cores and set thread priorities accordingly below
-        int pageDiff = getWidgetPageLoadPriority(page);
-        if (pageDiff <= 0) {
-            return Process.THREAD_PRIORITY_LESS_FAVORABLE;
-        } else if (pageDiff <= 1) {
-            return Process.THREAD_PRIORITY_LOWEST;
-        } else {
-            return Process.THREAD_PRIORITY_LOWEST;
-        }
-    }
-    private int getSleepForPage(int page) {
-        int pageDiff = getWidgetPageLoadPriority(page);
-        return Math.max(0, pageDiff * sPageSleepDelay);
-    }
-    /**
-     * Creates and executes a new AsyncTask to load a page of widget previews.
-     */
-    private void prepareLoadWidgetPreviewsTask(int page, ArrayList<Object> widgets,
-            int cellWidth, int cellHeight, int cellCountX) {
-
-        // Prune all tasks that are no longer needed
-        Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator();
-        while (iter.hasNext()) {
-            AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next();
-            int taskPage = task.page;
-            if (taskPage < getAssociatedLowerPageBound(mCurrentPage) ||
-                    taskPage > getAssociatedUpperPageBound(mCurrentPage)) {
-                task.cancel(false);
-                iter.remove();
-            } else {
-                task.setThreadPriority(getThreadPriorityForPage(taskPage));
-            }
-        }
-
-        // We introduce a slight delay to order the loading of side pages so that we don't thrash
-        final int sleepMs = getSleepForPage(page);
-        AsyncTaskPageData pageData = new AsyncTaskPageData(page, widgets, cellWidth, cellHeight,
-            new AsyncTaskCallback() {
-                @Override
-                public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) {
-                    try {
-                        try {
-                            Thread.sleep(sleepMs);
-                        } catch (Exception e) {}
-                        loadWidgetPreviewsInBackground(task, data);
-                    } finally {
-                        if (task.isCancelled()) {
-                            data.cleanup(true);
-                        }
-                    }
-                }
-            },
-            new AsyncTaskCallback() {
-                @Override
-                public void run(AppsCustomizeAsyncTask task, AsyncTaskPageData data) {
-                    mRunningTasks.remove(task);
-                    if (task.isCancelled()) return;
-                    // do cleanup inside onSyncWidgetPageItems
-                    onSyncWidgetPageItems(data, false);
-                }
-            }, getWidgetPreviewLoader());
-
-        // Ensure that the task is appropriately prioritized and runs in parallel
-        AppsCustomizeAsyncTask t = new AppsCustomizeAsyncTask(page,
-                AsyncTaskPageData.Type.LoadWidgetPreviewData);
-        t.setThreadPriority(getThreadPriorityForPage(page));
-        t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, pageData);
-        mRunningTasks.add(t);
-    }
-
-    /*
-     * Widgets PagedView implementation
-     */
-    private void setupPage(PagedViewGridLayout layout) {
-        // Note: We force a measure here to get around the fact that when we do layout calculations
-        // immediately after syncing, we don't have a proper width.
-        int widthSpec = MeasureSpec.makeMeasureSpec(mContentWidth, MeasureSpec.AT_MOST);
-        int heightSpec = MeasureSpec.makeMeasureSpec(mContentHeight, MeasureSpec.AT_MOST);
-
-        Drawable bg = getContext().getResources().getDrawable(R.drawable.quantum_panel_dark);
-        if (bg != null) {
-            bg.setAlpha(mPageBackgroundsVisible ? 255 : 0);
-            layout.setBackground(bg);
-        }
-        layout.measure(widthSpec, heightSpec);
-    }
-
-    public void syncWidgetPageItems(final int page, final boolean immediate) {
-        int numItemsPerPage = mWidgetCountX * mWidgetCountY;
-
-        final PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page);
-
-        // Calculate the dimensions of each cell we are giving to each widget
-        final ArrayList<Object> items = new ArrayList<Object>();
-        int contentWidth = mContentWidth - layout.getPaddingLeft() - layout.getPaddingRight();
-        final int cellWidth = contentWidth / mWidgetCountX;
-        int contentHeight = mContentHeight - layout.getPaddingTop() - layout.getPaddingBottom();
-
-        final int cellHeight = contentHeight / mWidgetCountY;
-
-        // Prepare the set of widgets to load previews for in the background
-        int offset = page * numItemsPerPage;
-        for (int i = offset; i < Math.min(offset + numItemsPerPage, mWidgets.size()); ++i) {
-            items.add(mWidgets.get(i));
-        }
-
-        // Prepopulate the pages with the other widget info, and fill in the previews later
-        layout.setColumnCount(layout.getCellCountX());
-        for (int i = 0; i < items.size(); ++i) {
-            Object rawInfo = items.get(i);
-            PendingAddItemInfo createItemInfo = null;
-            PagedViewWidget widget = (PagedViewWidget) mLayoutInflater.inflate(
-                    R.layout.apps_customize_widget, layout, false);
-
-            if (rawInfo instanceof LauncherAppWidgetProviderInfo) {
-                // Fill in the widget information
-                LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) rawInfo;
-                createItemInfo = new PendingAddWidgetInfo(info, null);
-
-                widget.applyFromAppWidgetProviderInfo(info, -1, getWidgetPreviewLoader());
-                widget.setTag(createItemInfo);
-                widget.setShortPressListener(this);
-            } else if (rawInfo instanceof ResolveInfo) {
-                // Fill in the shortcuts information
-                ResolveInfo info = (ResolveInfo) rawInfo;
-                createItemInfo = new PendingAddShortcutInfo(info.activityInfo);
-                createItemInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
-                createItemInfo.componentName = new ComponentName(info.activityInfo.packageName,
-                        info.activityInfo.name);
-                widget.applyFromResolveInfo(mPackageManager, info, getWidgetPreviewLoader());
-                widget.setTag(createItemInfo);
-            }
-
-            widget.setOnClickListener(this);
-            widget.setOnLongClickListener(this);
-            widget.setOnTouchListener(this);
-            widget.setOnKeyListener(this);
-
-            // Layout each widget
-            int ix = i % mWidgetCountX;
-            int iy = i / mWidgetCountX;
-
-            if (ix > 0) {
-                View border = widget.findViewById(R.id.left_border);
-                border.setVisibility(View.VISIBLE);
-            }
-            if (ix < mWidgetCountX - 1) {
-                View border = widget.findViewById(R.id.right_border);
-                border.setVisibility(View.VISIBLE);
-            }
-
-            GridLayout.LayoutParams lp = new GridLayout.LayoutParams(
-                    GridLayout.spec(iy, GridLayout.START),
-                    GridLayout.spec(ix, GridLayout.TOP));
-            lp.width = cellWidth;
-            lp.height = cellHeight;
-            lp.setGravity(Gravity.TOP | Gravity.START);
-            layout.addView(widget, lp);
-        }
-
-        // wait until a call on onLayout to start loading, because
-        // PagedViewWidget.getPreviewSize() will return 0 if it hasn't been laid out
-        // TODO: can we do a measure/layout immediately?
-        layout.setOnLayoutListener(new Runnable() {
-            public void run() {
-                // Load the widget previews
-                int maxPreviewWidth = cellWidth;
-                int maxPreviewHeight = cellHeight;
-                if (layout.getChildCount() > 0) {
-                    PagedViewWidget w = (PagedViewWidget) layout.getChildAt(0);
-                    int[] maxSize = w.getPreviewSize();
-                    maxPreviewWidth = maxSize[0];
-                    maxPreviewHeight = maxSize[1];
-                }
-
-                getWidgetPreviewLoader().setPreviewSize(
-                        maxPreviewWidth, maxPreviewHeight, mWidgetSpacingLayout);
-                if (immediate) {
-                    AsyncTaskPageData data = new AsyncTaskPageData(page, items,
-                            maxPreviewWidth, maxPreviewHeight, null, null, getWidgetPreviewLoader());
-                    loadWidgetPreviewsInBackground(null, data);
-                    onSyncWidgetPageItems(data, immediate);
-                } else {
-                    if (mInTransition) {
-                        mDeferredPrepareLoadWidgetPreviewsTasks.add(this);
-                    } else {
-                        prepareLoadWidgetPreviewsTask(page, items,
-                                maxPreviewWidth, maxPreviewHeight, mWidgetCountX);
-                    }
-                }
-                layout.setOnLayoutListener(null);
-            }
-        });
-    }
-    private void loadWidgetPreviewsInBackground(AppsCustomizeAsyncTask task,
-            AsyncTaskPageData data) {
-        // loadWidgetPreviewsInBackground can be called without a task to load a set of widget
-        // previews synchronously
-        if (task != null) {
-            // Ensure that this task starts running at the correct priority
-            task.syncThreadPriority();
-        }
-
-        // Load each of the widget/shortcut previews
-        ArrayList<Object> items = data.items;
-        ArrayList<Bitmap> images = data.generatedImages;
-        int count = items.size();
-        for (int i = 0; i < count; ++i) {
-            if (task != null) {
-                // Ensure we haven't been cancelled yet
-                if (task.isCancelled()) break;
-                // Before work on each item, ensure that this task is running at the correct
-                // priority
-                task.syncThreadPriority();
-            }
-
-            images.add(getWidgetPreviewLoader().getPreview(items.get(i)));
-        }
-    }
-
-    private void onSyncWidgetPageItems(AsyncTaskPageData data, boolean immediatelySyncItems) {
-        if (!immediatelySyncItems && mInTransition) {
-            mDeferredSyncWidgetPageItems.add(data);
-            return;
-        }
-        try {
-            int page = data.page;
-            PagedViewGridLayout layout = (PagedViewGridLayout) getPageAt(page);
-
-            ArrayList<Object> items = data.items;
-            int count = items.size();
-            for (int i = 0; i < count; ++i) {
-                PagedViewWidget widget = (PagedViewWidget) layout.getChildAt(i);
-                if (widget != null) {
-                    Bitmap preview = data.generatedImages.get(i);
-                    widget.applyPreview(new FastBitmapDrawable(preview), i);
-                }
-            }
-
-            enableHwLayersOnVisiblePages();
-
-            // Update all thread priorities
-            Iterator<AppsCustomizeAsyncTask> iter = mRunningTasks.iterator();
-            while (iter.hasNext()) {
-                AppsCustomizeAsyncTask task = (AppsCustomizeAsyncTask) iter.next();
-                int pageIndex = task.page;
-                task.setThreadPriority(getThreadPriorityForPage(pageIndex));
-            }
-        } finally {
-            data.cleanup(false);
-        }
-    }
-
-    @Override
-    public void syncPages() {
-        disablePagedViewAnimations();
-
-        removeAllViews();
-        cancelAllTasks();
-
-        Context context = getContext();
-        if (mContentType == ContentType.Applications) {
-            for (int i = 0; i < mNumAppsPages; ++i) {
-                AppsCustomizeCellLayout layout = new AppsCustomizeCellLayout(context);
-                setupPage(layout);
-                addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT,
-                        LayoutParams.MATCH_PARENT));
-            }
-        } else if (mContentType == ContentType.Widgets) {
-            for (int j = 0; j < mNumWidgetPages; ++j) {
-                PagedViewGridLayout layout = new PagedViewGridLayout(context, mWidgetCountX,
-                        mWidgetCountY);
-                setupPage(layout);
-                addView(layout, new PagedView.LayoutParams(LayoutParams.MATCH_PARENT,
-                        LayoutParams.MATCH_PARENT));
-            }
-        } else {
-            throw new RuntimeException("Invalid ContentType");
-        }
-
-        enablePagedViewAnimations();
-    }
-
-    @Override
-    public void syncPageItems(int page, boolean immediate) {
-        if (mContentType == ContentType.Widgets) {
-            syncWidgetPageItems(page, immediate);
-        } else {
-            syncAppsPageItems(page, immediate);
-        }
-    }
-
-    // We want our pages to be z-ordered such that the further a page is to the left, the higher
-    // it is in the z-order. This is important to insure touch events are handled correctly.
-    View getPageAt(int index) {
-        return getChildAt(indexToPage(index));
-    }
-
-    @Override
-    protected int indexToPage(int index) {
-        return getChildCount() - index - 1;
-    }
-
-    // In apps customize, we have a scrolling effect which emulates pulling cards off of a stack.
-    @Override
-    protected void screenScrolled(int screenCenter) {
-        super.screenScrolled(screenCenter);
-        enableHwLayersOnVisiblePages();
-    }
-
-    private void enableHwLayersOnVisiblePages() {
-        final int screenCount = getChildCount();
-
-        getVisiblePages(mTempVisiblePagesRange);
-        int leftScreen = mTempVisiblePagesRange[0];
-        int rightScreen = mTempVisiblePagesRange[1];
-        int forceDrawScreen = -1;
-        if (leftScreen == rightScreen) {
-            // make sure we're caching at least two pages always
-            if (rightScreen < screenCount - 1) {
-                rightScreen++;
-                forceDrawScreen = rightScreen;
-            } else if (leftScreen > 0) {
-                leftScreen--;
-                forceDrawScreen = leftScreen;
-            }
-        } else {
-            forceDrawScreen = leftScreen + 1;
-        }
-
-        for (int i = 0; i < screenCount; i++) {
-            final View layout = (View) getPageAt(i);
-            if (!(leftScreen <= i && i <= rightScreen &&
-                    (i == forceDrawScreen || shouldDrawChild(layout)))) {
-                layout.setLayerType(LAYER_TYPE_NONE, null);
-            }
-        }
-
-        for (int i = 0; i < screenCount; i++) {
-            final View layout = (View) getPageAt(i);
-
-            if (leftScreen <= i && i <= rightScreen &&
-                    (i == forceDrawScreen || shouldDrawChild(layout))) {
-                if (layout.getLayerType() != LAYER_TYPE_HARDWARE) {
-                    layout.setLayerType(LAYER_TYPE_HARDWARE, null);
-                }
-            }
-        }
-    }
-
-    protected void overScroll(float amount) {
-        dampedOverScroll(amount);
-    }
-
-    /**
-     * Used by the parent to get the content width to set the tab bar to
-     * @return
-     */
-    public int getPageContentWidth() {
-        return mContentWidth;
-    }
-
-    @Override
-    protected void onPageEndMoving() {
-        super.onPageEndMoving();
-        mForceDrawAllChildrenNextFrame = true;
-        // We reset the save index when we change pages so that it will be recalculated on next
-        // rotation
-        mSaveInstanceStateItemIndex = -1;
-    }
-
-    /*
-     * AllAppsView implementation
-     */
-    public void setup(Launcher launcher, DragController dragController) {
-        mLauncher = launcher;
-        mDragController = dragController;
-    }
-
-    /**
-     * We should call thise method whenever the core data changes (mApps, mWidgets) so that we can
-     * appropriately determine when to invalidate the PagedView page data.  In cases where the data
-     * has yet to be set, we can requestLayout() and wait for onDataReady() to be called in the
-     * next onMeasure() pass, which will trigger an invalidatePageData() itself.
-     */
-    private void invalidateOnDataChange() {
-        if (!isDataReady()) {
-            // The next layout pass will trigger data-ready if both widgets and apps are set, so
-            // request a layout to trigger the page data when ready.
-            requestLayout();
-        } else {
-            cancelAllTasks();
-            invalidatePageData();
-        }
-    }
-
-    public void setApps(ArrayList<AppInfo> list) {
-        if (!LauncherAppState.isDisableAllApps()) {
-            mApps = list;
-            Collections.sort(mApps, LauncherModel.getAppNameComparator());
-            updatePageCountsAndInvalidateData();
-        }
-    }
-
-    public ArrayList<AppInfo> getApps() {
-        return mApps;
-    }
-
-    private void addAppsWithoutInvalidate(ArrayList<AppInfo> list) {
-        // We add it in place, in alphabetical order
-        int count = list.size();
-        for (int i = 0; i < count; ++i) {
-            AppInfo info = list.get(i);
-            int index = Collections.binarySearch(mApps, info, LauncherModel.getAppNameComparator());
-            if (index < 0) {
-                mApps.add(-(index + 1), info);
-            }
-        }
-    }
-    public void addApps(ArrayList<AppInfo> list) {
-        if (!LauncherAppState.isDisableAllApps()) {
-            addAppsWithoutInvalidate(list);
-            updatePageCountsAndInvalidateData();
-        }
-    }
-    private int findAppByComponent(List<AppInfo> list, AppInfo item) {
-        ComponentName removeComponent = item.intent.getComponent();
-        int length = list.size();
-        for (int i = 0; i < length; ++i) {
-            AppInfo info = list.get(i);
-            if (info.user.equals(item.user)
-                    && info.intent.getComponent().equals(removeComponent)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-    private void removeAppsWithoutInvalidate(ArrayList<AppInfo> list) {
-        // loop through all the apps and remove apps that have the same component
-        int length = list.size();
-        for (int i = 0; i < length; ++i) {
-            AppInfo info = list.get(i);
-            int removeIndex = findAppByComponent(mApps, info);
-            if (removeIndex > -1) {
-                mApps.remove(removeIndex);
-            }
-        }
-    }
-    public void removeApps(ArrayList<AppInfo> appInfos) {
-        if (!LauncherAppState.isDisableAllApps()) {
-            removeAppsWithoutInvalidate(appInfos);
-            updatePageCountsAndInvalidateData();
-        }
-    }
-    public void updateApps(ArrayList<AppInfo> list) {
-        // We remove and re-add the updated applications list because it's properties may have
-        // changed (ie. the title), and this will ensure that the items will be in their proper
-        // place in the list.
-        if (!LauncherAppState.isDisableAllApps()) {
-            removeAppsWithoutInvalidate(list);
-            addAppsWithoutInvalidate(list);
-            updatePageCountsAndInvalidateData();
-        }
-    }
-
-    public void reset() {
-        // If we have reset, then we should not continue to restore the previous state
-        mSaveInstanceStateItemIndex = -1;
-
-        if (mContentType != ContentType.Applications) {
-            setContentType(ContentType.Applications);
-        }
-
-        if (mCurrentPage != 0) {
-            invalidatePageData(0);
-        }
-    }
-
-    private AppsCustomizeTabHost getTabHost() {
-        return (AppsCustomizeTabHost) mLauncher.findViewById(R.id.apps_customize_pane);
-    }
-
-    public void dumpState() {
-        // TODO: Dump information related to current list of Applications, Widgets, etc.
-        AppInfo.dumpApplicationInfoList(TAG, "mApps", mApps);
-        dumpAppWidgetProviderInfoList(TAG, "mWidgets", mWidgets);
-    }
-
-    private void dumpAppWidgetProviderInfoList(String tag, String label,
-            ArrayList<Object> list) {
-        Log.d(tag, label + " size=" + list.size());
-        for (Object i: list) {
-            if (i instanceof AppWidgetProviderInfo) {
-                AppWidgetProviderInfo info = (AppWidgetProviderInfo) i;
-                Log.d(tag, "   label=\"" + info.label + "\" previewImage=" + info.previewImage
-                        + " resizeMode=" + info.resizeMode + " configure=" + info.configure
-                        + " initialLayout=" + info.initialLayout
-                        + " minWidth=" + info.minWidth + " minHeight=" + info.minHeight);
-            } else if (i instanceof ResolveInfo) {
-                ResolveInfo info = (ResolveInfo) i;
-                Log.d(tag, "   label=\"" + info.loadLabel(mPackageManager) + "\" icon="
-                        + info.icon);
-            }
-        }
-    }
-
-    public void surrender() {
-        // TODO: If we are in the middle of any process (ie. for holographic outlines, etc) we
-        // should stop this now.
-
-        // Stop all background tasks
-        cancelAllTasks();
-    }
-
-    /*
-     * We load an extra page on each side to prevent flashes from scrolling and loading of the
-     * widget previews in the background with the AsyncTasks.
-     */
-    final static int sLookBehindPageCount = 2;
-    final static int sLookAheadPageCount = 2;
-    protected int getAssociatedLowerPageBound(int page) {
-        final int count = getChildCount();
-        int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1);
-        int windowMinIndex = Math.max(Math.min(page - sLookBehindPageCount, count - windowSize), 0);
-        return windowMinIndex;
-    }
-    protected int getAssociatedUpperPageBound(int page) {
-        final int count = getChildCount();
-        int windowSize = Math.min(count, sLookBehindPageCount + sLookAheadPageCount + 1);
-        int windowMaxIndex = Math.min(Math.max(page + sLookAheadPageCount, windowSize - 1),
-                count - 1);
-        return windowMaxIndex;
-    }
-
-    protected String getCurrentPageDescription() {
-        int page = (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
-        int stringId = R.string.default_scroll_format;
-        int count = 0;
-
-        if (mContentType == ContentType.Applications) {
-            stringId = R.string.apps_customize_apps_scroll_format;
-            count = mNumAppsPages;
-        } else if (mContentType == ContentType.Widgets) {
-            stringId = R.string.apps_customize_widgets_scroll_format;
-            count = mNumWidgetPages;
-        } else {
-            throw new RuntimeException("Invalid ContentType");
-        }
-
-        return String.format(getContext().getString(stringId), page + 1, count);
-    }
-}
diff --git a/src/com/android/launcher3/AppsCustomizeTabHost.java b/src/com/android/launcher3/AppsCustomizeTabHost.java
deleted file mode 100644
index a271712..0000000
--- a/src/com/android/launcher3/AppsCustomizeTabHost.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright (C) 2011 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.Rect;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.FrameLayout;
-
-public class AppsCustomizeTabHost extends FrameLayout implements LauncherTransitionable, Insettable  {
-    static final String LOG_TAG = "AppsCustomizeTabHost";
-
-    private static final String APPS_TAB_TAG = "APPS";
-    private static final String WIDGETS_TAB_TAG = "WIDGETS";
-
-    private AppsCustomizePagedView mPagedView;
-    private View mContent;
-    private boolean mInTransition = false;
-
-    private final Rect mInsets = new Rect();
-
-    public AppsCustomizeTabHost(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    /**
-     * Convenience methods to select specific tabs.  We want to set the content type immediately
-     * in these cases, but we note that we still call setCurrentTabByTag() so that the tab view
-     * reflects the new content (but doesn't do the animation and logic associated with changing
-     * tabs manually).
-     */
-    void setContentTypeImmediate(AppsCustomizePagedView.ContentType type) {
-        mPagedView.setContentType(type);
-    }
-
-    public void setCurrentTabFromContent(AppsCustomizePagedView.ContentType type) {
-        setContentTypeImmediate(type);
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        mInsets.set(insets);
-        LayoutParams flp = (LayoutParams) mContent.getLayoutParams();
-        flp.topMargin = insets.top;
-        flp.bottomMargin = insets.bottom;
-        flp.leftMargin = insets.left;
-        flp.rightMargin = insets.right;
-        mContent.setLayoutParams(flp);
-    }
-
-    /**
-     * Setup the tab host and create all necessary tabs.
-     */
-    @Override
-    protected void onFinishInflate() {
-        mPagedView = (AppsCustomizePagedView) findViewById(R.id.apps_customize_pane_content);
-        mContent = findViewById(R.id.content);
-    }
-
-    public String getContentTag() {
-        return getTabTagForContentType(mPagedView.getContentType());
-    }
-
-    /**
-     * Returns the content type for the specified tab tag.
-     */
-    public AppsCustomizePagedView.ContentType getContentTypeForTabTag(String tag) {
-        if (tag.equals(APPS_TAB_TAG)) {
-            return AppsCustomizePagedView.ContentType.Applications;
-        } else if (tag.equals(WIDGETS_TAB_TAG)) {
-            return AppsCustomizePagedView.ContentType.Widgets;
-        }
-        return AppsCustomizePagedView.ContentType.Applications;
-    }
-
-    /**
-     * Returns the tab tag for a given content type.
-     */
-    public String getTabTagForContentType(AppsCustomizePagedView.ContentType type) {
-        if (type == AppsCustomizePagedView.ContentType.Applications) {
-            return APPS_TAB_TAG;
-        } else if (type == AppsCustomizePagedView.ContentType.Widgets) {
-            return WIDGETS_TAB_TAG;
-        }
-        return APPS_TAB_TAG;
-    }
-
-    /**
-     * Disable focus on anything under this view in the hierarchy if we are not visible.
-     */
-    @Override
-    public int getDescendantFocusability() {
-        if (getVisibility() != View.VISIBLE) {
-            return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
-        }
-        return super.getDescendantFocusability();
-    }
-
-    void reset() {
-        // Reset immediately
-        mPagedView.reset();
-    }
-
-    void trimMemory() {
-        mPagedView.trimMemory();
-    }
-
-    public void onWindowVisible() {
-        if (getVisibility() == VISIBLE) {
-            mContent.setVisibility(VISIBLE);
-            // We unload the widget previews when the UI is hidden, so need to reload pages
-            // Load the current page synchronously, and the neighboring pages asynchronously
-            mPagedView.loadAssociatedPages(mPagedView.getCurrentPage(), true);
-            mPagedView.loadAssociatedPages(mPagedView.getCurrentPage());
-        }
-    }
-    @Override
-    public ViewGroup getContent() {
-        return mPagedView;
-    }
-
-    public boolean isInTransition() {
-        return mInTransition;
-    }
-
-    /* LauncherTransitionable overrides */
-    @Override
-    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
-        mPagedView.onLauncherTransitionPrepare(l, animated, toWorkspace);
-        mInTransition = true;
-
-        if (toWorkspace) {
-            // Going from All Apps -> Workspace
-            setVisibilityOfSiblingsWithLowerZOrder(VISIBLE);
-        } else {
-            // Going from Workspace -> All Apps
-            mContent.setVisibility(VISIBLE);
-
-            // Make sure the current page is loaded (we start loading the side pages after the
-            // transition to prevent slowing down the animation)
-            // TODO: revisit this
-            mPagedView.loadAssociatedPages(mPagedView.getCurrentPage());
-        }
-    }
-
-    @Override
-    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
-        mPagedView.onLauncherTransitionStart(l, animated, toWorkspace);
-    }
-
-    @Override
-    public void onLauncherTransitionStep(Launcher l, float t) {
-        mPagedView.onLauncherTransitionStep(l, t);
-    }
-
-    @Override
-    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
-        mPagedView.onLauncherTransitionEnd(l, animated, toWorkspace);
-        mInTransition = false;
-
-        if (!toWorkspace) {
-            // Make sure adjacent pages are loaded (we wait until after the transition to
-            // prevent slowing down the animation)
-            mPagedView.loadAssociatedPages(mPagedView.getCurrentPage());
-
-            // Opening apps, need to announce what page we are on.
-            AccessibilityManager am = (AccessibilityManager)
-                    getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
-            if (am.isEnabled()) {
-                // Notify the user when the page changes
-                announceForAccessibility(mPagedView.getCurrentPageDescription());
-            }
-
-            // Going from Workspace -> All Apps
-            // NOTE: We should do this at the end since we check visibility state in some of the
-            // cling initialization/dismiss code above.
-            setVisibilityOfSiblingsWithLowerZOrder(INVISIBLE);
-        }
-    }
-
-    private void setVisibilityOfSiblingsWithLowerZOrder(int visibility) {
-        ViewGroup parent = (ViewGroup) getParent();
-        if (parent == null) return;
-
-        View overviewPanel = ((Launcher) getContext()).getOverviewPanel();
-        final int count = parent.getChildCount();
-        if (!isChildrenDrawingOrderEnabled()) {
-            for (int i = 0; i < count; i++) {
-                final View child = parent.getChildAt(i);
-                if (child == this) {
-                    break;
-                } else {
-                    if (child.getVisibility() == GONE || child == overviewPanel) {
-                        continue;
-                    }
-                    child.setVisibility(visibility);
-                }
-            }
-        } else {
-            throw new RuntimeException("Failed; can't get z-order of views");
-        }
-    }
-}
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
new file mode 100644
index 0000000..954c59f
--- /dev/null
+++ b/src/com/android/launcher3/AppsGridAdapter.java
@@ -0,0 +1,258 @@
+package com.android.launcher3;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.launcher3.util.Thunk;
+
+import java.util.List;
+
+
+/**
+ * The grid view adapter of all the apps.
+ */
+class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
+
+    public static final String TAG = "AppsGridAdapter";
+
+    private static final int SECTION_BREAK_VIEW_TYPE = 0;
+    private static final int ICON_VIEW_TYPE = 1;
+    private static final int EMPTY_VIEW_TYPE = 2;
+
+    /**
+     * ViewHolder for each icon.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public View mContent;
+        public boolean mIsSectionRow;
+        public boolean mIsEmptyRow;
+
+        public ViewHolder(View v, boolean isSectionRow, boolean isEmptyRow) {
+            super(v);
+            mContent = v;
+            mIsSectionRow = isSectionRow;
+            mIsEmptyRow = isEmptyRow;
+        }
+    }
+
+    /**
+     * Helper class to size the grid items.
+     */
+    public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
+        @Override
+        public int getSpanSize(int position) {
+            if (mApps.hasNoFilteredResults()) {
+                // Empty view spans full width
+                return mAppsPerRow;
+            }
+
+            if (mApps.getAdapterItems().get(position).isSectionHeader) {
+                // Section break spans full width
+                return mAppsPerRow;
+            } else {
+                return 1;
+            }
+        }
+    }
+
+    /**
+     * Helper class to draw the section headers
+     */
+    public class GridItemDecoration extends RecyclerView.ItemDecoration {
+
+        @Override
+        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+            List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
+            if (items.isEmpty()) {
+                return;
+            }
+
+            for (int i = 0; i < parent.getChildCount(); i++) {
+                View child = parent.getChildAt(i);
+                ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
+                if (holder != null) {
+                    GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
+                            child.getLayoutParams();
+                    if (!holder.mIsSectionRow && !holder.mIsEmptyRow && !lp.isItemRemoved()) {
+                        if (items.get(holder.getPosition() - 1).isSectionHeader) {
+                            // Draw at the parent
+                            AlphabeticalAppsList.AdapterItem item =
+                                    items.get(holder.getPosition());
+                            String section = item.sectionName;
+                            mSectionTextPaint.getTextBounds(section, 0, section.length(),
+                                    mTmpBounds);
+                            if (mIsRtl) {
+                                int left = parent.getWidth() - mPaddingStart - mStartMargin;
+                                c.drawText(section, left + (mStartMargin - mTmpBounds.width()) / 2,
+                                        child.getTop() + (2 * child.getPaddingTop()) +
+                                                mTmpBounds.height(), mSectionTextPaint);
+                            } else {
+                                int left = mPaddingStart;
+                                c.drawText(section, left + (mStartMargin - mTmpBounds.width()) / 2,
+                                    child.getTop() + (2 * child.getPaddingTop()) +
+                                            mTmpBounds.height(), mSectionTextPaint);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+                RecyclerView.State state) {
+            // Do nothing
+        }
+    }
+
+    private LayoutInflater mLayoutInflater;
+    @Thunk AlphabeticalAppsList mApps;
+    private GridLayoutManager mGridLayoutMgr;
+    private GridSpanSizer mGridSizer;
+    private GridItemDecoration mItemDecoration;
+    private View.OnTouchListener mTouchListener;
+    private View.OnClickListener mIconClickListener;
+    private View.OnLongClickListener mIconLongClickListener;
+    @Thunk int mAppsPerRow;
+    @Thunk boolean mIsRtl;
+    private String mEmptySearchText;
+
+    // Section drawing
+    @Thunk int mPaddingStart;
+    @Thunk int mStartMargin;
+    @Thunk Paint mSectionTextPaint;
+    @Thunk Rect mTmpBounds = new Rect();
+
+
+    public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
+            View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
+            View.OnLongClickListener iconLongClickListener) {
+        Resources res = context.getResources();
+        mApps = apps;
+        mAppsPerRow = appsPerRow;
+        mGridSizer = new GridSpanSizer();
+        mGridLayoutMgr = new GridLayoutManager(context, appsPerRow, GridLayoutManager.VERTICAL,
+                false);
+        mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
+        mItemDecoration = new GridItemDecoration();
+        mLayoutInflater = LayoutInflater.from(context);
+        mTouchListener = touchListener;
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+        mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
+        mPaddingStart = res.getDimensionPixelSize(R.dimen.apps_container_inset);
+        mSectionTextPaint = new Paint();
+        mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
+                R.dimen.apps_view_section_text_size));
+        mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color));
+        mSectionTextPaint.setAntiAlias(true);
+    }
+
+    /**
+     * Sets the number of apps per row.
+     */
+    public void setNumAppsPerRow(int appsPerRow) {
+        mAppsPerRow = appsPerRow;
+        mGridLayoutMgr.setSpanCount(appsPerRow);
+    }
+
+    /**
+     * Sets whether we are in RTL mode.
+     */
+    public void setRtl(boolean rtl) {
+        mIsRtl = rtl;
+    }
+
+    /**
+     * Sets the text to show when there are no apps.
+     */
+    public void setEmptySearchText(String query) {
+        mEmptySearchText = query;
+    }
+
+    /**
+     * Returns the grid layout manager.
+     */
+    public GridLayoutManager getLayoutManager(Context context) {
+        return mGridLayoutMgr;
+    }
+
+    /**
+     * Returns the item decoration for the recycler view.
+     */
+    public RecyclerView.ItemDecoration getItemDecoration() {
+        return mItemDecoration;
+    }
+
+    /**
+     * Returns the left padding for the recycler view.
+     */
+    public int getContentMarginStart() {
+        return mStartMargin;
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case EMPTY_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
+                        false), false /* isSectionRow */, true /* isEmptyRow */);
+            case SECTION_BREAK_VIEW_TYPE:
+                return new ViewHolder(new View(parent.getContext()), true /* isSectionRow */,
+                        false /* isEmptyRow */);
+            case ICON_VIEW_TYPE:
+                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
+                        R.layout.apps_grid_row_icon_view, parent, false);
+                icon.setOnTouchListener(mTouchListener);
+                icon.setOnClickListener(mIconClickListener);
+                icon.setOnLongClickListener(mIconLongClickListener);
+                icon.setFocusable(true);
+                return new ViewHolder(icon, false /* isSectionRow */, false /* isEmptyRow */);
+            default:
+                throw new RuntimeException("Unexpected view type");
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int position) {
+        switch (holder.getItemViewType()) {
+            case ICON_VIEW_TYPE:
+                AppInfo info = mApps.getAdapterItems().get(position).appInfo;
+                BubbleTextView icon = (BubbleTextView) holder.mContent;
+                icon.applyFromApplicationInfo(info);
+                break;
+            case EMPTY_VIEW_TYPE:
+                TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
+                emptyViewText.setText(mEmptySearchText);
+                break;
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        if (mApps.hasNoFilteredResults()) {
+            // For the empty view
+            return 1;
+        }
+        return mApps.getAdapterItems().size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (mApps.hasNoFilteredResults()) {
+            return EMPTY_VIEW_TYPE;
+        } else if (mApps.getAdapterItems().get(position).isSectionHeader) {
+            return SECTION_BREAK_VIEW_TYPE;
+        }
+        return ICON_VIEW_TYPE;
+    }
+}
diff --git a/src/com/android/launcher3/AppsListAdapter.java b/src/com/android/launcher3/AppsListAdapter.java
new file mode 100644
index 0000000..ffd3092
--- /dev/null
+++ b/src/com/android/launcher3/AppsListAdapter.java
@@ -0,0 +1,143 @@
+package com.android.launcher3;
+
+import android.content.Context;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * The linear list view adapter for all the apps.
+ */
+class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
+
+    /**
+     * ViewHolder for each row.
+     */
+    public static class ViewHolder extends RecyclerView.ViewHolder {
+        public View mContent;
+
+        public ViewHolder(View v) {
+            super(v);
+            mContent = v;
+        }
+    }
+
+    private static final int SECTION_BREAK_VIEW_TYPE = 0;
+    private static final int ICON_VIEW_TYPE = 1;
+    private static final int EMPTY_VIEW_TYPE = 2;
+
+    private LayoutInflater mLayoutInflater;
+    private AlphabeticalAppsList mApps;
+    private View.OnTouchListener mTouchListener;
+    private View.OnClickListener mIconClickListener;
+    private View.OnLongClickListener mIconLongClickListener;
+    private String mEmptySearchText;
+
+    public AppsListAdapter(Context context, AlphabeticalAppsList apps,
+            View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
+            View.OnLongClickListener iconLongClickListener) {
+        mApps = apps;
+        mLayoutInflater = LayoutInflater.from(context);
+        mTouchListener = touchListener;
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+    }
+
+    public RecyclerView.LayoutManager getLayoutManager(Context context) {
+        return new LinearLayoutManager(context);
+    }
+
+    /**
+     * Sets the text to show when there are no apps.
+     */
+    public void setEmptySearchText(String query) {
+        mEmptySearchText = query;
+    }
+
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case EMPTY_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
+                        false));
+            case SECTION_BREAK_VIEW_TYPE:
+                return new ViewHolder(new View(parent.getContext()));
+            case ICON_VIEW_TYPE:
+                // Inflate the row and all the icon children necessary
+                ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.apps_list_row_view,
+                        parent, false);
+                BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
+                        R.layout.apps_list_row_icon_view, row, false);
+                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0,
+                        ViewGroup.LayoutParams.WRAP_CONTENT, 1);
+                lp.gravity = Gravity.CENTER_VERTICAL;
+                icon.setLayoutParams(lp);
+                icon.setOnTouchListener(mTouchListener);
+                icon.setOnClickListener(mIconClickListener);
+                icon.setOnLongClickListener(mIconLongClickListener);
+                icon.setFocusable(true);
+                row.addView(icon);
+                return new ViewHolder(row);
+            default:
+                throw new RuntimeException("Unexpected view type");
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(ViewHolder holder, int position) {
+        switch (holder.getItemViewType()) {
+            case ICON_VIEW_TYPE:
+                AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+                ViewGroup content = (ViewGroup) holder.mContent;
+                String sectionDescription = item.sectionName;
+
+                // Bind the section header
+                boolean showSectionHeader = true;
+                if (position > 0) {
+                    AlphabeticalAppsList.AdapterItem prevItem =
+                            mApps.getAdapterItems().get(position - 1);
+                    showSectionHeader = prevItem.isSectionHeader;
+                }
+                TextView tv = (TextView) content.findViewById(R.id.section);
+                if (showSectionHeader) {
+                    tv.setText(sectionDescription);
+                    tv.setVisibility(View.VISIBLE);
+                } else {
+                    tv.setVisibility(View.INVISIBLE);
+                }
+
+                // Bind the icon
+                BubbleTextView icon = (BubbleTextView) content.getChildAt(1);
+                icon.applyFromApplicationInfo(item.appInfo);
+                break;
+            case EMPTY_VIEW_TYPE:
+                TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
+                emptyViewText.setText(mEmptySearchText);
+                break;
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        if (mApps.hasNoFilteredResults()) {
+            // For the empty view
+            return 1;
+        }
+        return mApps.getAdapterItems().size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (mApps.hasNoFilteredResults()) {
+            return EMPTY_VIEW_TYPE;
+        } else if (mApps.getAdapterItems().get(position).isSectionHeader) {
+            return SECTION_BREAK_VIEW_TYPE;
+        }
+        return ICON_VIEW_TYPE;
+    }
+}
diff --git a/src/com/android/launcher3/AutoInstallsLayout.java b/src/com/android/launcher3/AutoInstallsLayout.java
index 3820660..dac79a8 100644
--- a/src/com/android/launcher3/AutoInstallsLayout.java
+++ b/src/com/android/launcher3/AutoInstallsLayout.java
@@ -37,6 +37,7 @@
 
 import com.android.launcher3.LauncherProvider.SqlArguments;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.util.Thunk;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -44,6 +45,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Locale;
 
 /**
  * Layout parsing code for auto installs layout
@@ -56,6 +58,11 @@
     static final String ACTION_LAUNCHER_CUSTOMIZATION =
             "android.autoinstalls.config.action.PLAY_AUTO_INSTALL";
 
+    /**
+     * Layout resource which also includes grid size and hotseat count, e.g., default_layout_6x6_h5
+     */
+    private static final String FORMATTED_LAYOUT_RES_WITH_HOSTEAT = "default_layout_%dx%d_h%s";
+    private static final String FORMATTED_LAYOUT_RES = "default_layout_%dx%d";
     private static final String LAYOUT_RES = "default_layout";
 
     static AutoInstallsLayout get(Context context, AppWidgetHost appWidgetHost,
@@ -65,15 +72,39 @@
         if (customizationApkInfo == null) {
             return null;
         }
+        return get(context, customizationApkInfo.first, customizationApkInfo.second,
+                appWidgetHost, callback);
+    }
 
-        String pkg = customizationApkInfo.first;
-        Resources res = customizationApkInfo.second;
-        int layoutId = res.getIdentifier(LAYOUT_RES, "xml", pkg);
+    static AutoInstallsLayout get(Context context, String pkg, Resources targetRes,
+            AppWidgetHost appWidgetHost, LayoutParserCallback callback) {
+        DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
+
+        // Try with grid size and hotseat count
+        String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT,
+                (int) grid.numColumns, (int) grid.numRows, (int) grid.numHotseatIcons);
+        int layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
+
+        // Try with only grid size
+        if (layoutId == 0) {
+            Log.d(TAG, "Formatted layout: " + layoutName
+                    + " not found. Trying layout without hosteat");
+            layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES,
+                    (int) grid.numColumns, (int) grid.numRows);
+            layoutId = targetRes.getIdentifier(layoutName, "xml", pkg);
+        }
+
+        // Try the default layout
+        if (layoutId == 0) {
+            Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout");
+            layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml", pkg);
+        }
+
         if (layoutId == 0) {
             Log.e(TAG, "Layout definition not found in package: " + pkg);
             return null;
         }
-        return new AutoInstallsLayout(context, appWidgetHost, callback, res, layoutId,
+        return new AutoInstallsLayout(context, appWidgetHost, callback, targetRes, layoutId,
                 TAG_WORKSPACE);
     }
 
@@ -114,9 +145,9 @@
     private static final String ACTION_APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE =
             "com.android.launcher.action.APPWIDGET_DEFAULT_WORKSPACE_CONFIGURE";
 
-    private final Context mContext;
-    private final AppWidgetHost mAppWidgetHost;
-    private final LayoutParserCallback mCallback;
+    @Thunk final Context mContext;
+    @Thunk final AppWidgetHost mAppWidgetHost;
+    protected final LayoutParserCallback mCallback;
 
     protected final PackageManager mPackageManager;
     protected final Resources mSourceRes;
@@ -125,14 +156,21 @@
     private final int mHotseatAllAppsRank;
 
     private final long[] mTemp = new long[2];
-    private final ContentValues mValues;
-    private final String mRootTag;
+    @Thunk final ContentValues mValues;
+    protected final String mRootTag;
 
     protected SQLiteDatabase mDb;
 
     public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
             LayoutParserCallback callback, Resources res,
             int layoutId, String rootTag) {
+        this(context, appWidgetHost, callback, res, layoutId, rootTag,
+                LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile().hotseatAllAppsRank);
+    }
+
+    public AutoInstallsLayout(Context context, AppWidgetHost appWidgetHost,
+            LayoutParserCallback callback, Resources res,
+            int layoutId, String rootTag, int hotseatAllAppsRank) {
         mContext = context;
         mAppWidgetHost = appWidgetHost;
         mCallback = callback;
@@ -143,8 +181,7 @@
 
         mSourceRes = res;
         mLayoutId = layoutId;
-        mHotseatAllAppsRank = LauncherAppState.getInstance()
-                .getDynamicGrid().getDeviceProfile().hotseatAllAppsRank;
+        mHotseatAllAppsRank = hotseatAllAppsRank;
     }
 
     /**
@@ -569,7 +606,7 @@
             // failed to add, and less than 2 were actually added
             if (folderItems.size() < 2) {
                 // Delete the folder
-                Uri uri = Favorites.getContentUri(folderId, false);
+                Uri uri = Favorites.getContentUri(folderId);
                 SqlArguments args = new SqlArguments(uri, null, null);
                 mDb.delete(args.table, args.where, args.args);
                 addedId = -1;
@@ -642,7 +679,7 @@
         long insertAndCheck(SQLiteDatabase db, ContentValues values);
     }
 
-    private static void copyInteger(ContentValues from, ContentValues to, String key) {
+    @Thunk static void copyInteger(ContentValues from, ContentValues to, String key) {
         to.put(key, from.getAsInteger(key));
     }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index f9255e6..ae6ebba 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -28,11 +28,14 @@
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.util.TypedValue;
+import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
 import android.widget.TextView;
 
+import com.android.launcher3.IconCache.IconLoadRequest;
+
 /**
  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
  * because we want to make the bubble taller than the text and TextView's clip is
@@ -49,7 +52,7 @@
     private static final int SHADOW_SMALL_COLOUR = 0xCC000000;
     static final float PADDING_V = 3.0f;
 
-
+    private Drawable mIcon;
     private final Drawable mBackground;
     private final CheckLongPressHelper mLongPressHelper;
     private final HolographicOutlineHelper mOutlineHelper;
@@ -62,13 +65,19 @@
 
     private float mSlop;
 
-    private int mTextColor;
+    private final boolean mDeferShadowGenerationOnTouch;
     private final boolean mCustomShadowsEnabled;
-    private boolean mIsTextVisible;
+    private final boolean mLayoutHorizontal;
+    private final int mIconSize;
+    private final int mIconPaddingSize;
+    private final int mTextSize;
+    private int mTextColor;
 
     private boolean mStayPressed;
     private boolean mIgnorePressedStateChange;
 
+    private IconLoadRequest mIconLoadRequest;
+
     public BubbleTextView(Context context) {
         this(context, null, 0);
     }
@@ -79,10 +88,21 @@
 
     public BubbleTextView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
 
         TypedArray a = context.obtainStyledAttributes(attrs,
                 R.styleable.BubbleTextView, defStyle, 0);
         mCustomShadowsEnabled = a.getBoolean(R.styleable.BubbleTextView_customShadows, true);
+        mLayoutHorizontal = a.getBoolean(R.styleable.BubbleTextView_layoutHorizontal, false);
+        mIconSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconSizeOverride,
+                grid.allAppsIconSizePx);
+        mIconPaddingSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_iconPaddingOverride,
+                grid.iconDrawablePaddingPx);
+        mTextSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_textSizeOverride,
+                grid.allAppsIconTextSizePx);
+        mDeferShadowGenerationOnTouch =
+                a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false);
         a.recycle();
 
         if (mCustomShadowsEnabled) {
@@ -92,6 +112,12 @@
         } else {
             mBackground = null;
         }
+
+        // If we are laying out horizontal, then center the text vertically
+        if (mLayoutHorizontal) {
+            setGravity(Gravity.CENTER_VERTICAL);
+        }
+
         mLongPressHelper = new CheckLongPressHelper(this);
 
         mOutlineHelper = HolographicOutlineHelper.obtain(getContext());
@@ -106,9 +132,7 @@
         super.onFinishInflate();
 
         // Ensure we are using the right text size
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-        setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
+        setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
     }
 
     public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,
@@ -119,16 +143,11 @@
     public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,
             boolean setDefaultPadding, boolean promiseStateChanged) {
         Bitmap b = info.getIcon(iconCache);
-        LauncherAppState app = LauncherAppState.getInstance();
 
         FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b);
         iconDrawable.setGhostModeEnabled(info.isDisabled != 0);
 
-        setCompoundDrawables(null, iconDrawable, null, null);
-        if (setDefaultPadding) {
-            DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-            setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
-        }
+        setIcon(iconDrawable, mIconSize, setDefaultPadding ? mIconPaddingSize : -1);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
         }
@@ -141,20 +160,17 @@
     }
 
     public void applyFromApplicationInfo(AppInfo info) {
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-
-        Drawable topDrawable = Utilities.createIconDrawable(info.iconBitmap);
-        topDrawable.setBounds(0, 0, grid.allAppsIconSizePx, grid.allAppsIconSizePx);
-        setCompoundDrawables(null, topDrawable, null, null);
-        setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
+        setIcon(Utilities.createIconDrawable(info.iconBitmap), mIconSize, mIconPaddingSize);
         setText(info.title);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
         }
-        setTag(info);
-    }
+        // We don't need to check the info since it's not a ShortcutInfo
+        super.setTag(info);
 
+        // Verify high res immediately
+        verifyHighRes();
+    }
 
     @Override
     protected boolean setFrame(int left, int top, int right, int bottom) {
@@ -186,10 +202,19 @@
         }
     }
 
+    /** Returns the icon for this view. */
+    public Drawable getIcon() {
+        return mIcon;
+    }
+
+    /** Returns whether the layout is horizontal. */
+    public boolean isLayoutHorizontal() {
+        return mLayoutHorizontal;
+    }
+
     private void updateIconState() {
-        Drawable top = getCompoundDrawables()[1];
-        if (top instanceof FastBitmapDrawable) {
-            ((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed);
+        if (mIcon instanceof FastBitmapDrawable) {
+            ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed);
         }
     }
 
@@ -204,7 +229,7 @@
                 // So that the pressed outline is visible immediately on setStayPressed(),
                 // we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
                 // to create it)
-                if (mPressedBackground == null) {
+                if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) {
                     mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
                 }
 
@@ -233,6 +258,10 @@
         mStayPressed = stayPressed;
         if (!stayPressed) {
             mPressedBackground = null;
+        } else {
+            if (mPressedBackground == null) {
+                mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
+            }
         }
 
         // Only show the shadow effect when persistent pressed state is set.
@@ -325,10 +354,9 @@
         super.onAttachedToWindow();
 
         if (mBackground != null) mBackground.setCallback(this);
-        Drawable top = getCompoundDrawables()[1];
 
-        if (top instanceof PreloadIconDrawable) {
-            ((PreloadIconDrawable) top).applyPreloaderTheme(getPreloaderTheme());
+        if (mIcon instanceof PreloadIconDrawable) {
+            ((PreloadIconDrawable) mIcon).applyPreloaderTheme(getPreloaderTheme());
         }
         mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
     }
@@ -358,16 +386,6 @@
         } else {
             super.setTextColor(res.getColor(android.R.color.transparent));
         }
-        mIsTextVisible = visible;
-    }
-
-    public boolean isTextVisible() {
-        return mIsTextVisible;
-    }
-
-    @Override
-    protected boolean onSetAlpha(int alpha) {
-        return true;
     }
 
     @Override
@@ -385,15 +403,13 @@
                     ((info.hasStatusFlag(ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE) ?
                             info.getInstallProgress() : 0)) : 100;
 
-            Drawable[] drawables = getCompoundDrawables();
-            Drawable top = drawables[1];
-            if (top != null) {
+            if (mIcon != null) {
                 final PreloadIconDrawable preloadDrawable;
-                if (top instanceof PreloadIconDrawable) {
-                    preloadDrawable = (PreloadIconDrawable) top;
+                if (mIcon instanceof PreloadIconDrawable) {
+                    preloadDrawable = (PreloadIconDrawable) mIcon;
                 } else {
-                    preloadDrawable = new PreloadIconDrawable(top, getPreloaderTheme());
-                    setCompoundDrawables(drawables[0], preloadDrawable, drawables[2], drawables[3]);
+                    preloadDrawable = new PreloadIconDrawable(mIcon, getPreloaderTheme());
+                    setIcon(preloadDrawable, mIconSize, -1);
                 }
 
                 preloadDrawable.setLevel(progressLevel);
@@ -417,4 +433,61 @@
         }
         return theme;
     }
+
+    /**
+     * Sets the icon for this view based on the layout direction.
+     */
+    private Drawable setIcon(Drawable icon, int iconSize, int drawablePadding) {
+        mIcon = icon;
+        if (iconSize != -1) {
+            mIcon.setBounds(0, 0, iconSize, iconSize);
+        }
+        if (mLayoutHorizontal) {
+            setCompoundDrawablesRelative(mIcon, null, null, null);
+        } else {
+            setCompoundDrawablesRelative(null, mIcon, null, null);
+        }
+        if (drawablePadding != -1) {
+            setCompoundDrawablePadding(drawablePadding);
+        }
+        return icon;
+    }
+
+    /**
+     * Applies the item info if it is same as what the view is pointing to currently.
+     */
+    public void reapplyItemInfo(final ItemInfo info) {
+        if (getTag() == info) {
+            mIconLoadRequest = null;
+            if (info instanceof AppInfo) {
+                applyFromApplicationInfo((AppInfo) info);
+            } else if (info instanceof ShortcutInfo) {
+                applyFromShortcutInfo((ShortcutInfo) info,
+                        LauncherAppState.getInstance().getIconCache(), false);
+            }
+        }
+    }
+
+    /**
+     * Verifies that the current icon is high-res otherwise posts a request to load the icon.
+     */
+    public void verifyHighRes() {
+        if (mIconLoadRequest != null) {
+            mIconLoadRequest.cancel();
+            mIconLoadRequest = null;
+        }
+        if (getTag() instanceof AppInfo) {
+            AppInfo info = (AppInfo) getTag();
+            if (info.usingLowResIcon) {
+                mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
+                        .updateIconInBackground(BubbleTextView.this, info);
+            }
+        } else if (getTag() instanceof ShortcutInfo) {
+            ShortcutInfo info = (ShortcutInfo) getTag();
+            if (info.usingLowResIcon) {
+                mIconLoadRequest = LauncherAppState.getInstance().getIconCache()
+                        .updateIconInBackground(BubbleTextView.this, info);
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 019f86c..5b39908 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -17,19 +17,29 @@
 package com.android.launcher3;
 
 import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
 import android.widget.TextView;
 
+import com.android.launcher3.R;
+import com.android.launcher3.util.Thunk;
 
 /**
  * Implements a DropTarget.
  */
-public class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener {
+public abstract class ButtonDropTarget extends TextView implements DropTarget, DragController.DragListener {
+
+    private static int DRAG_VIEW_DROP_DURATION = 285;
 
     protected final int mTransitionDuration;
 
@@ -44,6 +54,9 @@
     /** The paint applied to the drag view on hover */
     protected int mHoverColor = 0;
 
+    protected ColorStateList mOriginalTextColor;
+    protected TransitionDrawable mDrawable;
+
     public ButtonDropTarget(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -56,12 +69,37 @@
         mBottomDragPadding = r.getDimensionPixelSize(R.dimen.drop_target_drag_padding);
     }
 
-    void setLauncher(Launcher launcher) {
-        mLauncher = launcher;
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mOriginalTextColor = getTextColors();
+
+        // Remove the text in the Phone UI in landscape
+        int orientation = getResources().getConfiguration().orientation;
+        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            if (!LauncherAppState.getInstance().isScreenLarge()) {
+                setText("");
+            }
+        }
     }
 
-    public boolean acceptDrop(DragObject d) {
-        return false;
+    protected void setDrawable(int resId) {
+        // Get the hover color
+        mDrawable = (TransitionDrawable) getCurrentDrawable();
+
+        if (mDrawable == null) {
+            // TODO: investigate why this is ever happening. Presently only on one known device.
+            mDrawable = (TransitionDrawable) getResources().getDrawable(resId);
+            setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null);
+        }
+
+        if (null != mDrawable) {
+            mDrawable.setCrossFadeEnabled(true);
+        }
+    }
+
+    public void setLauncher(Launcher launcher) {
+        mLauncher = launcher;
     }
 
     public void setSearchDropTargetBar(SearchDropTargetBar searchDropTargetBar) {
@@ -78,37 +116,94 @@
         return null;
     }
 
-    public void onDrop(DragObject d) {
-    }
+    @Override
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) { }
 
-    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
-        // Do nothing
-    }
-
-    public void onDragEnter(DragObject d) {
+    @Override
+    public final void onDragEnter(DragObject d) {
         d.dragView.setColor(mHoverColor);
+        mDrawable.startTransition(mTransitionDuration);
+        setTextColor(mHoverColor);
     }
 
+    @Override
     public void onDragOver(DragObject d) {
         // Do nothing
     }
 
-    public void onDragExit(DragObject d) {
-        d.dragView.setColor(0);
+    protected void resetHoverColor() {
+        mDrawable.resetTransition();
+        setTextColor(mOriginalTextColor);
     }
 
-    public void onDragStart(DragSource source, Object info, int dragAction) {
-        // Do nothing
+    @Override
+    public final void onDragExit(DragObject d) {
+        if (!d.dragComplete) {
+            d.dragView.setColor(0);
+            resetHoverColor();
+        } else {
+            // Restore the hover color
+            d.dragView.setColor(mHoverColor);
+        }
     }
 
+    @Override
+    public final void onDragStart(DragSource source, Object info, int dragAction) {
+        mActive = supportsDrop(source, info);
+        mDrawable.resetTransition();
+        setTextColor(mOriginalTextColor);
+        ((ViewGroup) getParent()).setVisibility(mActive ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public final boolean acceptDrop(DragObject dragObject) {
+        return supportsDrop(dragObject.dragSource, dragObject.dragInfo);
+    }
+
+    protected abstract boolean supportsDrop(DragSource source, Object info);
+
+    @Override
     public boolean isDropEnabled() {
         return mActive;
     }
 
+    @Override
     public void onDragEnd() {
-        // Do nothing
+        mActive = false;
     }
 
+    /**
+     * On drop animate the dropView to the icon.
+     */
+    @Override
+    public void onDrop(final DragObject d) {
+        final DragLayer dragLayer = mLauncher.getDragLayer();
+        final Rect from = new Rect();
+        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
+
+        int width = mDrawable.getIntrinsicWidth();
+        int height = mDrawable.getIntrinsicHeight();
+        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
+                width, height);
+        final float scale = (float) to.width() / from.width();
+        mSearchDropTargetBar.deferOnDragEnd();
+
+        Runnable onAnimationEndRunnable = new Runnable() {
+            @Override
+            public void run() {
+                completeDrop(d);
+                mSearchDropTargetBar.onDragEnd();
+                mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null);
+            }
+        };
+        dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
+                DRAG_VIEW_DROP_DURATION, new DecelerateInterpolator(2),
+                new LinearInterpolator(), onAnimationEndRunnable,
+                DragLayer.ANIMATION_END_DISAPPEAR, null);
+    }
+
+    @Thunk abstract void completeDrop(DragObject d);
+
     @Override
     public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) {
         super.getHitRect(outRect);
@@ -120,10 +215,10 @@
     }
 
     private boolean isRtl() {
-        return (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
+        return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
     }
 
-    Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) {
+    protected Rect getIconRect(int viewWidth, int viewHeight, int drawableWidth, int drawableHeight) {
         DragLayer dragLayer = mLauncher.getDragLayer();
 
         // Find the rect to animate to (the view is center aligned)
@@ -157,6 +252,7 @@
         return to;
     }
 
+    @Override
     public void getLocationInDragLayer(int[] loc) {
         mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
     }
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index e6865b2..f4afb95 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -22,6 +22,7 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -33,7 +34,12 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
 import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.widget.ExploreByTouchHelper;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.SparseArray;
@@ -41,35 +47,40 @@
 import android.view.View;
 import android.view.ViewDebug;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Animation;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.LayoutAnimationController;
 
 import com.android.launcher3.FolderIcon.FolderRingAnimator;
+import com.android.launcher3.LauncherAccessibilityDelegate.DragType;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Stack;
 
 public class CellLayout extends ViewGroup {
     static final String TAG = "CellLayout";
 
     private Launcher mLauncher;
-    private int mCellWidth;
-    private int mCellHeight;
+    @Thunk int mCellWidth;
+    @Thunk int mCellHeight;
     private int mFixedCellWidth;
     private int mFixedCellHeight;
 
-    private int mCountX;
-    private int mCountY;
+    @Thunk int mCountX;
+    @Thunk int mCountY;
 
     private int mOriginalWidthGap;
     private int mOriginalHeightGap;
-    private int mWidthGap;
-    private int mHeightGap;
+    @Thunk int mWidthGap;
+    @Thunk int mHeightGap;
     private int mMaxGap;
     private boolean mDropPending = false;
     private boolean mIsDragTarget = true;
@@ -77,7 +88,7 @@
     // These are temporary variables to prevent having to allocate a new object just to
     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
     private final int[] mTmpXY = new int[2];
-    private final int[] mTmpPoint = new int[2];
+    @Thunk final int[] mTmpPoint = new int[2];
     int[] mTempLocation = new int[2];
 
     boolean[][] mOccupied;
@@ -114,8 +125,8 @@
 
     // These arrays are used to implement the drag visualization on x-large screens.
     // They are used as circular arrays, indexed by mDragOutlineCurrent.
-    private Rect[] mDragOutlines = new Rect[4];
-    private float[] mDragOutlineAlphas = new float[mDragOutlines.length];
+    @Thunk Rect[] mDragOutlines = new Rect[4];
+    @Thunk float[] mDragOutlineAlphas = new float[mDragOutlines.length];
     private InterruptibleInOutAnimator[] mDragOutlineAnims =
             new InterruptibleInOutAnimator[mDragOutlines.length];
 
@@ -125,7 +136,7 @@
 
     private final FastBitmapView mTouchFeedbackView;
 
-    private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new
+    @Thunk HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new
             HashMap<CellLayout.LayoutParams, Animator>();
     private HashMap<View, ReorderPreviewAnimation>
             mShakeAnimators = new HashMap<View, ReorderPreviewAnimation>();
@@ -156,7 +167,7 @@
 
     private static final float REORDER_PREVIEW_MAGNITUDE = 0.12f;
     private static final int REORDER_ANIMATION_DURATION = 150;
-    private float mReorderPreviewAnimationMagnitude;
+    @Thunk float mReorderPreviewAnimationMagnitude;
 
     private ArrayList<View> mIntersectingViews = new ArrayList<View>();
     private Rect mOccupiedRect = new Rect();
@@ -169,6 +180,14 @@
 
     private final static Paint sPaint = new Paint();
 
+    // Related to accessible drag and drop
+    DragAndDropAccessibilityDelegate mTouchHelper = new DragAndDropAccessibilityDelegate(this);
+    private boolean mUseTouchHelper = false;
+    OnClickListener mOldClickListener = null;
+    OnClickListener mOldWorkspaceListener = null;
+    @Thunk int mDownX = 0;
+    @Thunk int mDownY = 0;
+
     public CellLayout(Context context) {
         this(context, null);
     }
@@ -294,6 +313,301 @@
         addView(mShortcutsAndWidgets);
     }
 
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public void enableAccessibleDrag(boolean enable) {
+        mUseTouchHelper = enable;
+        if (!enable) {
+            ViewCompat.setAccessibilityDelegate(this, null);
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+            setOnClickListener(mLauncher);
+        } else {
+            ViewCompat.setAccessibilityDelegate(this, mTouchHelper);
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+            getShortcutsAndWidgets().setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+            setOnClickListener(mTouchHelper);
+        }
+
+        // Invalidate the accessibility hierarchy
+        if (getParent() != null) {
+            getParent().notifySubtreeAccessibilityStateChanged(
+                    this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+        }
+    }
+
+    @Override
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // Always attempt to dispatch hover events to accessibility first.
+        if (mUseTouchHelper && mTouchHelper.dispatchHoverEvent(event)) {
+            return true;
+        }
+        return super.dispatchHoverEvent(event);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mDownX = (int) event.getX();
+            mDownY = (int) event.getY();
+        }
+        return super.dispatchTouchEvent(event);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (mUseTouchHelper ||
+                (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev))) {
+            return true;
+        }
+        return false;
+    }
+
+    class DragAndDropAccessibilityDelegate extends ExploreByTouchHelper implements OnClickListener {
+        private final Rect mTempRect = new Rect();
+
+        public DragAndDropAccessibilityDelegate(View forView) {
+            super(forView);
+        }
+
+        private int getViewIdAt(float x, float y) {
+            if (x < 0 || y < 0 || x > getMeasuredWidth() || y > getMeasuredHeight()) {
+                return ExploreByTouchHelper.INVALID_ID;
+            }
+
+            // Map coords to cell
+            int cellX = (int) Math.floor(x / (mCellWidth + mWidthGap));
+            int cellY = (int) Math.floor(y / (mCellHeight + mHeightGap));
+
+            // Map cell to id
+            int id = cellX * mCountY + cellY;
+            return id;
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            return nearestDropLocation(getViewIdAt(x, y));
+        }
+
+        protected int nearestDropLocation(int id) {
+            int count = mCountX * mCountY;
+            for (int delta = 0; delta < count; delta++) {
+                if (id + delta <= (count - 1)) {
+                    int target = intersectsValidDropTarget(id + delta);
+                    if (target >= 0) {
+                        return target;
+                    }
+                } else if (id - delta >= 0) {
+                    int target = intersectsValidDropTarget(id - delta);
+                    if (target >= 0) {
+                        return target;
+                    }
+                }
+            }
+            return ExploreByTouchHelper.INVALID_ID;
+        }
+
+        /**
+         * Find the virtual view id corresponding to the top left corner of any drop region by which
+         * the passed id is contained. For an icon, this is simply
+         *
+         * @param id the id we're interested examining (ie. does it fit there?)
+         * @return the view id of the top left corner of a valid drop region or -1 if there is no
+         *         such valid region. For the icon, this can just be -1 or id.
+         */
+        protected int intersectsValidDropTarget(int id) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            if (delegate == null) {
+                return -1;
+            }
+
+            int y = id % mCountY;
+            int x = id / mCountY;
+            LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo();
+
+            if (dragInfo.dragType == DragType.WIDGET) {
+                // For a widget, every cell must be vacant. In addition, we will return any valid
+                // drop target by which the passed id is contained.
+                boolean fits = false;
+
+                // These represent the amount that we can back off if we hit a problem. They
+                // get consumed as we move up and to the right, trying new regions.
+                int spanX = dragInfo.info.spanX;
+                int spanY = dragInfo.info.spanY;
+
+                for (int m = 0; m < spanX; m++) {
+                    for (int n = 0; n < spanY; n++) {
+
+                        fits = true;
+                        int x0 = x - m;
+                        int y0 = y - n;
+
+                        if (x0 < 0 || y0 < 0) continue;
+
+                        for (int i = x0; i < x0 + spanX; i++) {
+                            if (!fits) break;
+                            for (int j = y0; j < y0 + spanY; j++) {
+                                if (i >= mCountX || j >= mCountY || mOccupied[i][j]) {
+                                    fits = false;
+                                    break;
+                                }
+                            }
+                        }
+                        if (fits) {
+                            return x0 * mCountY + y0;
+                        }
+                    }
+                }
+                return -1;
+            } else {
+                // For an icon, we simply check the view directly below
+                View child = getChildAt(x, y);
+                if (child == null || child == dragInfo.item) {
+                    // Empty cell. Good for an icon or folder.
+                    return id;
+                } else if (dragInfo.dragType != DragType.FOLDER) {
+                    // For icons, we can consider cells that have another icon or a folder.
+                    ItemInfo info = (ItemInfo) child.getTag();
+                    if (info instanceof AppInfo || info instanceof FolderInfo ||
+                            info instanceof ShortcutInfo) {
+                        return id;
+                    }
+                }
+                return -1;
+            }
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(List<Integer> virtualViews) {
+            // We create a virtual view for each cell of the grid
+            // The cell ids correspond to cells in reading order.
+            int nCells = mCountX * mCountY;
+
+            for (int i = 0; i < nCells; i++) {
+                if (intersectsValidDropTarget(i) >= 0) {
+                    virtualViews.add(i);
+                }
+            }
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int viewId, int action, Bundle args) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            if (delegate == null) {
+                return false;
+            }
+
+            if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
+                String confirmation = getConfirmationForIconDrop(viewId);
+                delegate.handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onClick(View arg0) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            if (delegate == null) {
+                return;
+            }
+
+            int viewId = getViewIdAt(mDownX, mDownY);
+
+            String confirmation = getConfirmationForIconDrop(viewId);
+            delegate.handleAccessibleDrop(CellLayout.this, getItemBounds(viewId), confirmation);
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int id, AccessibilityEvent event) {
+            if (id == ExploreByTouchHelper.INVALID_ID) {
+                throw new IllegalArgumentException("Invalid virtual view id");
+            }
+            // We're required to set something here.
+            event.setContentDescription("");
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(int id, AccessibilityNodeInfoCompat node) {
+            if (id == ExploreByTouchHelper.INVALID_ID) {
+                throw new IllegalArgumentException("Invalid virtual view id");
+            }
+
+            node.setContentDescription(getLocationDescriptionForIconDrop(id));
+            node.setBoundsInParent(getItemBounds(id));
+
+            node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+            node.setClickable(true);
+            node.setFocusable(true);
+        }
+
+        private String getLocationDescriptionForIconDrop(int id) {
+            LauncherAccessibilityDelegate delegate =
+                    LauncherAppState.getInstance().getAccessibilityDelegate();
+            if (delegate == null) {
+                return "";
+            }
+
+            int y = id % mCountY;
+            int x = id / mCountY;
+            LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo();
+
+            Resources res = getContext().getResources();
+            View child = getChildAt(x, y);
+            if (child == null || child == dragInfo.item) {
+                return res.getString(R.string.move_to_empty_cell, x, y);
+            } else {
+                ItemInfo info = (ItemInfo) child.getTag();
+                if (info instanceof AppInfo || info instanceof ShortcutInfo) {
+                    return res.getString(R.string.create_folder_with, info.title);
+                } else if (info instanceof FolderInfo) {
+                    return res.getString(R.string.add_to_folder, info.title);
+                }
+            }
+            return "";
+        }
+
+        private String getConfirmationForIconDrop(int id) {
+            LauncherAccessibilityDelegate delegate =
+                LauncherAppState.getInstance().getAccessibilityDelegate();
+            if (delegate == null) {
+                return "";
+            }
+
+            int y = id % mCountY;
+            int x = id / mCountY;
+            LauncherAccessibilityDelegate.DragInfo dragInfo = delegate.getDragInfo();
+
+            Resources res = getContext().getResources();
+            View child = getChildAt(x, y);
+            if (child == null || child == dragInfo.item) {
+                return res.getString(R.string.item_moved);
+            } else {
+                ItemInfo info = (ItemInfo) child.getTag();
+                if (info instanceof AppInfo || info instanceof ShortcutInfo) {
+                    return res.getString(R.string.folder_created);
+
+                } else if (info instanceof FolderInfo) {
+                    return res.getString(R.string.added_to_folder);
+                }
+            }
+            return "";
+        }
+
+        private Rect getItemBounds(int id) {
+            int cellY = id % mCountY;
+            int cellX = id / mCountY;
+            int x = getPaddingLeft() + (int) (cellX * (mCellWidth + mWidthGap));
+            int y = getPaddingTop() + (int) (cellY * (mCellHeight + mHeightGap));
+
+            Rect bounds = mTempRect;
+            bounds.set(x, y, x + mCellWidth, y + mCellHeight);
+            return bounds;
+        }
+    }
+
     public void enableHardwareLayer(boolean hasLayer) {
         mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint);
     }
@@ -578,11 +892,11 @@
         mInterceptTouchListener = listener;
     }
 
-    int getCountX() {
+    public int getCountX() {
         return mCountX;
     }
 
-    int getCountY() {
+    public int getCountY() {
         return mCountY;
     }
 
@@ -613,11 +927,7 @@
             if (lp.cellVSpan < 0) lp.cellVSpan = mCountY;
 
             child.setId(childId);
-            if (inLayout) {
-                mShortcutsAndWidgets.addView(child, index, lp, true);
-            } else {
-                mShortcutsAndWidgets.addView(child, index, lp, false);
-            }
+            mShortcutsAndWidgets.addView(child, index, lp, inLayout);
 
             if (markCells) markCellsAsOccupiedForView(child);
 
@@ -683,18 +993,6 @@
         mShortcutsAndWidgets.removeViewsInLayout(start, count);
     }
 
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        // First we clear the tag to ensure that on every touch down we start with a fresh slate,
-        // even in the case where we return early. Not clearing here was causing bugs whereby on
-        // long-press we'd end up picking up an item from a previous drag operation.
-        if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) {
-            return true;
-        }
-
-        return false;
-    }
-
     /**
      * Given a point, return the cell that strictly encloses that point
      * @param x X coordinate of the point
@@ -2272,7 +2570,7 @@
             }
         }
 
-        private void completeAnimationImmediately() {
+        @Thunk void completeAnimationImmediately() {
             if (a != null) {
                 a.cancel();
             }
@@ -2593,7 +2891,7 @@
         return mItemPlacementDirty;
     }
 
-    private class ItemConfiguration {
+    @Thunk class ItemConfiguration {
         HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>();
         private HashMap<View, CellAndSpan> savedMap = new HashMap<View, CellAndSpan>();
         ArrayList<View> sortedViews = new ArrayList<View>();
@@ -2727,7 +3025,7 @@
      *
      * @return True if a vacant cell of the specified dimension was found, false otherwise.
      */
-    boolean findCellForSpan(int[] cellXY, int spanX, int spanY) {
+    public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) {
         return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied);
     }
 
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
index 8114979..10ca6a3 100644
--- a/src/com/android/launcher3/CheckLongPressHelper.java
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -18,9 +18,11 @@
 
 import android.view.View;
 
+import com.android.launcher3.util.Thunk;
+
 public class CheckLongPressHelper {
-    private View mView;
-    private boolean mHasPerformedLongPress;
+    @Thunk View mView;
+    @Thunk boolean mHasPerformedLongPress;
     private CheckForLongPress mPendingCheckForLongPress;
 
     class CheckForLongPress implements Runnable {
diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java
new file mode 100644
index 0000000..3164179
--- /dev/null
+++ b/src/com/android/launcher3/CommonAppTypeParser.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2008 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.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.XmlResourceParser;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.backup.BackupProtos.Favorite;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * A class that parses content values corresponding to some common app types.
+ */
+public class CommonAppTypeParser implements LayoutParserCallback {
+    private static final String TAG = "CommonAppTypeParser";
+
+    // Including TARGET_NONE
+    public static final int SUPPORTED_TYPE_COUNT = 7;
+
+    private static final int RESTORE_FLAG_BIT_SHIFT = 4;
+
+
+    private final long mItemId;
+    private final int mResId;
+    private final Context mContext;
+
+    ContentValues parsedValues;
+    Intent parsedIntent;
+    String parsedTitle;
+
+    public CommonAppTypeParser(long itemId, int itemType, Context context) {
+        mItemId = itemId;
+        mContext = context;
+        mResId = getResourceForItemType(itemType);
+    }
+
+    @Override
+    public long generateNewItemId() {
+        return mItemId;
+    }
+
+    @Override
+    public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
+        parsedValues = values;
+
+        // Remove unwanted values
+        values.put(Favorites.ICON_TYPE, (Integer) null);
+        values.put(Favorites.ICON_PACKAGE, (String) null);
+        values.put(Favorites.ICON_RESOURCE, (String) null);
+        values.put(Favorites.ICON, (byte[]) null);
+        return 1;
+    }
+
+    /**
+     * Tries to find a suitable app to the provided app type.
+     */
+    public boolean findDefaultApp() {
+        if (mResId == 0) {
+            return false;
+        }
+
+        parsedIntent = null;
+        parsedValues = null;
+        new MyLayoutParser().parseValues();
+        return (parsedValues != null) && (parsedIntent != null);
+    }
+
+    private class MyLayoutParser extends DefaultLayoutParser {
+
+        public MyLayoutParser() {
+            super(CommonAppTypeParser.this.mContext, null, CommonAppTypeParser.this,
+                    CommonAppTypeParser.this.mContext.getResources(), mResId, TAG_RESOLVE, 0);
+        }
+
+        @Override
+        protected long addShortcut(String title, Intent intent, int type) {
+            if (type == Favorites.ITEM_TYPE_APPLICATION) {
+                parsedIntent = intent;
+                parsedTitle = title;
+            }
+            return super.addShortcut(title, intent, type);
+        }
+
+        public void parseValues() {
+            XmlResourceParser parser = mSourceRes.getXml(mLayoutId);
+            try {
+                beginDocument(parser, mRootTag);
+                new ResolveParser().parseAndAdd(parser);
+            } catch (IOException | XmlPullParserException e) {
+                Log.e(TAG, "Unable to parse default app info", e);
+            }
+            parser.close();
+        }
+    }
+
+    public static int getResourceForItemType(int type) {
+        switch (type) {
+            case Favorite.TARGET_PHONE:
+                return R.xml.app_target_phone;
+
+            case Favorite.TARGET_MESSENGER:
+                return R.xml.app_target_messenger;
+
+            case Favorite.TARGET_EMAIL:
+                return R.xml.app_target_email;
+
+            case Favorite.TARGET_BROWSER:
+                return R.xml.app_target_browser;
+
+            case Favorite.TARGET_GALLERY:
+                return R.xml.app_target_gallery;
+
+            case Favorite.TARGET_CAMERA:
+                return R.xml.app_target_camera;
+
+            default:
+                return 0;
+        }
+    }
+
+    public static int encodeItemTypeToFlag(int itemType) {
+        return itemType << RESTORE_FLAG_BIT_SHIFT;
+    }
+
+    public static int decodeItemTypeFromFlag(int flag) {
+        return (flag & ShortcutInfo.FLAG_RESTORED_APP_TYPE) >> RESTORE_FLAG_BIT_SHIFT;
+    }
+
+}
diff --git a/src/com/android/launcher3/DefaultLayoutParser.java b/src/com/android/launcher3/DefaultLayoutParser.java
index 986ae81..7b91c67 100644
--- a/src/com/android/launcher3/DefaultLayoutParser.java
+++ b/src/com/android/launcher3/DefaultLayoutParser.java
@@ -13,6 +13,7 @@
 import android.util.Log;
 
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.util.Thunk;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -28,15 +29,15 @@
 public class DefaultLayoutParser extends AutoInstallsLayout {
     private static final String TAG = "DefaultLayoutParser";
 
-    private static final String TAG_RESOLVE = "resolve";
+    protected static final String TAG_RESOLVE = "resolve";
     private static final String TAG_FAVORITES = "favorites";
-    private static final String TAG_FAVORITE = "favorite";
+    protected static final String TAG_FAVORITE = "favorite";
     private static final String TAG_APPWIDGET = "appwidget";
     private static final String TAG_SHORTCUT = "shortcut";
     private static final String TAG_FOLDER = "folder";
     private static final String TAG_PARTNER_FOLDER = "partner-folder";
 
-    private static final String ATTR_URI = "uri";
+    protected static final String ATTR_URI = "uri";
     private static final String ATTR_CONTAINER = "container";
     private static final String ATTR_SCREEN = "screen";
     private static final String ATTR_FOLDER_ITEMS = "folderItems";
@@ -44,7 +45,12 @@
     public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost,
             LayoutParserCallback callback, Resources sourceRes, int layoutId) {
         super(context, appWidgetHost, callback, sourceRes, layoutId, TAG_FAVORITES);
-        Log.e(TAG, "Default layout parser initialized");
+    }
+
+    public DefaultLayoutParser(Context context, AppWidgetHost appWidgetHost,
+            LayoutParserCallback callback, Resources sourceRes, int layoutId, String rootTag,
+            int hotseatAllAppsRank) {
+        super(context, appWidgetHost, callback, sourceRes, layoutId, rootTag, hotseatAllAppsRank);
     }
 
     @Override
@@ -52,7 +58,7 @@
         return getFolderElementsMap(mSourceRes);
     }
 
-    private HashMap<String, TagParser> getFolderElementsMap(Resources res) {
+    @Thunk HashMap<String, TagParser> getFolderElementsMap(Resources res) {
         HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
         parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
         parsers.put(TAG_SHORTCUT, new UriShortcutParser(res));
@@ -84,7 +90,7 @@
     /**
      * AppShortcutParser which also supports adding URI based intents
      */
-    private class AppShortcutWithUriParser extends AppShortcutParser {
+    @Thunk class AppShortcutWithUriParser extends AppShortcutParser {
 
         @Override
         protected long invalidPackageOrClass(XmlResourceParser parser) {
@@ -196,7 +202,7 @@
     /**
      * Contains a list of <favorite> nodes, and accepts the first successfully parsed node.
      */
-    private class ResolveParser implements TagParser {
+    protected class ResolveParser implements TagParser {
 
         private final AppShortcutWithUriParser mChildParser = new AppShortcutWithUriParser();
 
@@ -226,7 +232,7 @@
     /**
      * A parser which adds a folder whose contents come from partner apk.
      */
-    private class PartnerFolderParser implements TagParser {
+    @Thunk class PartnerFolderParser implements TagParser {
 
         @Override
         public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException,
@@ -252,7 +258,7 @@
     /**
      * An extension of FolderParser which allows adding items from a different xml.
      */
-    private class MyFolderParser extends FolderParser {
+    @Thunk class MyFolderParser extends FolderParser {
 
         @Override
         public long parseAndAdd(XmlResourceParser parser) throws XmlPullParserException,
diff --git a/src/com/android/launcher3/DeferredHandler.java b/src/com/android/launcher3/DeferredHandler.java
index a2d121d..eb7c26a 100644
--- a/src/com/android/launcher3/DeferredHandler.java
+++ b/src/com/android/launcher3/DeferredHandler.java
@@ -22,6 +22,8 @@
 import android.os.MessageQueue;
 import android.util.Pair;
 
+import com.android.launcher3.util.Thunk;
+
 import java.util.LinkedList;
 import java.util.ListIterator;
 
@@ -33,11 +35,11 @@
  * This class is fifo.
  */
 public class DeferredHandler {
-    private LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>();
+    @Thunk LinkedList<Pair<Runnable, Integer>> mQueue = new LinkedList<Pair<Runnable, Integer>>();
     private MessageQueue mMessageQueue = Looper.myQueue();
     private Impl mHandler = new Impl();
 
-    private class Impl extends Handler implements MessageQueue.IdleHandler {
+    @Thunk class Impl extends Handler implements MessageQueue.IdleHandler {
         public void handleMessage(Message msg) {
             Pair<Runnable, Integer> p;
             Runnable r;
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index ebe874f..aa3e66c 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -19,31 +19,22 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.annotation.TargetApi;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.drawable.TransitionDrawable;
 import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.UserManager;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.ViewGroup;
 import android.view.animation.AnimationUtils;
 import android.view.animation.DecelerateInterpolator;
-import android.view.animation.LinearInterpolator;
 
-import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.R;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.WidgetsContainerView;
 
 public class DeleteDropTarget extends ButtonDropTarget {
-    private static int DELETE_ANIMATION_DURATION = 285;
+
     private static int FLING_DELETE_ANIMATION_DURATION = 350;
     private static float FLING_TO_DELETE_FRICTION = 0.035f;
     private static int MODE_FLING_DELETE_TO_TRASH = 0;
@@ -51,13 +42,6 @@
 
     private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR;
 
-    private ColorStateList mOriginalTextColor;
-    private TransitionDrawable mUninstallDrawable;
-    private TransitionDrawable mRemoveDrawable;
-    private TransitionDrawable mCurrentDrawable;
-
-    private boolean mWaitingForUninstall = false;
-
     public DeleteDropTarget(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -69,271 +53,27 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-
-        // Get the drawable
-        mOriginalTextColor = getTextColors();
-
         // Get the hover color
-        Resources r = getResources();
-        mHoverColor = r.getColor(R.color.delete_target_hover_tint);
-        mUninstallDrawable = (TransitionDrawable) 
-                r.getDrawable(R.drawable.uninstall_target_selector);
-        mRemoveDrawable = (TransitionDrawable) r.getDrawable(R.drawable.remove_target_selector);
+        mHoverColor = getResources().getColor(R.color.delete_target_hover_tint);
 
-        mRemoveDrawable.setCrossFadeEnabled(true);
-        mUninstallDrawable.setCrossFadeEnabled(true);
-
-        // The current drawable is set to either the remove drawable or the uninstall drawable 
-        // and is initially set to the remove drawable, as set in the layout xml.
-        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
-
-        // Remove the text in the Phone UI in landscape
-        int orientation = getResources().getConfiguration().orientation;
-        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            if (!LauncherAppState.getInstance().isScreenLarge()) {
-                setText("");
-            }
-        }
+        setDrawable(R.drawable.remove_target_selector);
     }
 
-    private boolean isAllAppsApplication(DragSource source, Object info) {
-        return source.supportsAppInfoDropTarget() && (info instanceof AppInfo);
-    }
-    private boolean isAllAppsWidget(DragSource source, Object info) {
-        if (source instanceof AppsCustomizePagedView) {
-            if (info instanceof PendingAddItemInfo) {
-                PendingAddItemInfo addInfo = (PendingAddItemInfo) info;
-                switch (addInfo.itemType) {
-                    case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-                    case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
-                    case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
-                        return true;
-                }
-            }
-        }
-        return false;
-    }
-    private boolean isDragSourceWorkspaceOrFolder(DragObject d) {
-        return (d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder);
-    }
-
-    private void setHoverColor() {
-        if (mCurrentDrawable != null) {
-            mCurrentDrawable.startTransition(mTransitionDuration);
-        }
-        setTextColor(mHoverColor);
-    }
-    private void resetHoverColor() {
-        if (mCurrentDrawable != null) {
-            mCurrentDrawable.resetTransition();
-        }
-        setTextColor(mOriginalTextColor);
+    public static boolean willAcceptDrop(DragSource source, Object info) {
+        return (info instanceof ItemInfo) && source.supportsDeleteDropTarget();
     }
 
     @Override
-    public boolean acceptDrop(DragObject d) {
-        return willAcceptDrop(d.dragInfo);
-    }
-
-    public static boolean willAcceptDrop(Object info) {
-        if (info instanceof ItemInfo) {
-            ItemInfo item = (ItemInfo) info;
-            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET ||
-                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET ||
-                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) {
-                return true;
-            }
-
-            if (!LauncherAppState.isDisableAllApps() &&
-                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
-                return true;
-            }
-
-            if (!LauncherAppState.isDisableAllApps() &&
-                    item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
-                    item instanceof AppInfo) {
-                AppInfo appInfo = (AppInfo) info;
-                return (appInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0;
-            }
-
-            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
-                item instanceof ShortcutInfo) {
-                if (LauncherAppState.isDisableAllApps()) {
-                    ShortcutInfo shortcutInfo = (ShortcutInfo) info;
-                    return (shortcutInfo.flags & AppInfo.DOWNLOADED_FLAG) != 0;
-                } else {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-
-    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
-    @Override
-    public void onDragStart(DragSource source, Object info, int dragAction) {
-        boolean isVisible = true;
-        boolean useUninstallLabel = !LauncherAppState.isDisableAllApps() &&
-                isAllAppsApplication(source, info);
-        boolean useDeleteLabel = !useUninstallLabel && source.supportsDeleteDropTarget();
-
-        // If we are dragging an application from AppsCustomize, only show the control if we can
-        // delete the app (it was downloaded), and rename the string to "uninstall" in such a case.
-        // Hide the delete target if it is a widget from AppsCustomize.
-        if (!willAcceptDrop(info) || isAllAppsWidget(source, info)) {
-            isVisible = false;
-        }
-        if (useUninstallLabel) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
-                UserManager userManager = (UserManager)
-                        getContext().getSystemService(Context.USER_SERVICE);
-                Bundle restrictions = userManager.getUserRestrictions();
-                if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false)
-                        || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) {
-                    isVisible = false;
-                }
-            }
-        }
-
-        if (useUninstallLabel) {
-            setCompoundDrawablesRelativeWithIntrinsicBounds(mUninstallDrawable, null, null, null);
-        } else if (useDeleteLabel) {
-            setCompoundDrawablesRelativeWithIntrinsicBounds(mRemoveDrawable, null, null, null);
-        } else {
-            isVisible = false;
-        }
-        mCurrentDrawable = (TransitionDrawable) getCurrentDrawable();
-
-        mActive = isVisible;
-        resetHoverColor();
-        ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
-        if (isVisible && getText().length() > 0) {
-            setText(useUninstallLabel ? R.string.delete_target_uninstall_label
-                : R.string.delete_target_label);
-        }
+    protected boolean supportsDrop(DragSource source, Object info) {
+        return willAcceptDrop(source, info);
     }
 
     @Override
-    public void onDragEnd() {
-        super.onDragEnd();
-        mActive = false;
-    }
-
-    public void onDragEnter(DragObject d) {
-        super.onDragEnter(d);
-
-        setHoverColor();
-    }
-
-    public void onDragExit(DragObject d) {
-        super.onDragExit(d);
-
-        if (!d.dragComplete) {
-            resetHoverColor();
-        } else {
-            // Restore the hover color if we are deleting
-            d.dragView.setColor(mHoverColor);
-        }
-    }
-
-    private void animateToTrashAndCompleteDrop(final DragObject d) {
-        final DragLayer dragLayer = mLauncher.getDragLayer();
-        final Rect from = new Rect();
-        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
-
-        int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth();
-        int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight();
-        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
-                width, height);
-        final float scale = (float) to.width() / from.width();
-
-        mSearchDropTargetBar.deferOnDragEnd();
-        deferCompleteDropIfUninstalling(d);
-
-        Runnable onAnimationEndRunnable = new Runnable() {
-            @Override
-            public void run() {
-                completeDrop(d);
-                mSearchDropTargetBar.onDragEnd();
-                mLauncher.exitSpringLoadedDragModeDelayed(true, 0, null);
-            }
-        };
-        dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
-                DELETE_ANIMATION_DURATION, new DecelerateInterpolator(2),
-                new LinearInterpolator(), onAnimationEndRunnable,
-                DragLayer.ANIMATION_END_DISAPPEAR, null);
-    }
-
-    private void deferCompleteDropIfUninstalling(DragObject d) {
-        mWaitingForUninstall = false;
-        if (isUninstallFromWorkspace(d)) {
-            if (d.dragSource instanceof Folder) {
-                ((Folder) d.dragSource).deferCompleteDropAfterUninstallActivity();
-            } else if (d.dragSource instanceof Workspace) {
-                ((Workspace) d.dragSource).deferCompleteDropAfterUninstallActivity();
-            }
-            mWaitingForUninstall = true;
-        }
-    }
-
-    private boolean isUninstallFromWorkspace(DragObject d) {
-        if (LauncherAppState.isDisableAllApps() && isDragSourceWorkspaceOrFolder(d)
-                && (d.dragInfo instanceof ShortcutInfo)) {
-            ShortcutInfo shortcut = (ShortcutInfo) d.dragInfo;
-            // Only allow manifest shortcuts to initiate an un-install.
-            return !InstallShortcutReceiver.isValidShortcutLaunchIntent(shortcut.intent);
-        }
-        return false;
-    }
-
-    private void completeDrop(DragObject d) {
+    @Thunk void completeDrop(DragObject d) {
         ItemInfo item = (ItemInfo) d.dragInfo;
-        boolean wasWaitingForUninstall = mWaitingForUninstall;
-        mWaitingForUninstall = false;
-        if (isAllAppsApplication(d.dragSource, item)) {
-            uninstallApp(mLauncher, (AppInfo) item);
-        } else if (isUninstallFromWorkspace(d)) {
-            ShortcutInfo shortcut = (ShortcutInfo) item;
-            if (shortcut.intent != null && shortcut.intent.getComponent() != null) {
-                final ComponentName componentName = shortcut.intent.getComponent();
-                final DragSource dragSource = d.dragSource;
-                final UserHandleCompat user = shortcut.user;
-                mWaitingForUninstall = mLauncher.startApplicationUninstallActivity(
-                        componentName, shortcut.flags, user);
-                if (mWaitingForUninstall) {
-                    final Runnable checkIfUninstallWasSuccess = new Runnable() {
-                        @Override
-                        public void run() {
-                            mWaitingForUninstall = false;
-                            String packageName = componentName.getPackageName();
-                            boolean uninstallSuccessful = !AllAppsList.packageHasActivities(
-                                    getContext(), packageName, user);
-                            if (dragSource instanceof Folder) {
-                                ((Folder) dragSource).
-                                    onUninstallActivityReturned(uninstallSuccessful);
-                            } else if (dragSource instanceof Workspace) {
-                                ((Workspace) dragSource).
-                                    onUninstallActivityReturned(uninstallSuccessful);
-                            }
-                        }
-                    };
-                    mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess);
-                }
-            }
-        } else if (isDragSourceWorkspaceOrFolder(d)) {
+        if ((d.dragSource instanceof Workspace) || (d.dragSource instanceof Folder)) {
             removeWorkspaceOrFolderItem(mLauncher, item, null);
         }
-        if (wasWaitingForUninstall && !mWaitingForUninstall) {
-            if (d.dragSource instanceof Folder) {
-                ((Folder) d.dragSource).onUninstallActivityReturned(false);
-            } else if (d.dragSource instanceof Workspace) {
-                ((Workspace) d.dragSource).onUninstallActivityReturned(false);
-            }
-        }
-    }
-
-    public static void uninstallApp(Launcher launcher, AppInfo info) {
-        launcher.startApplicationUninstallActivity(info.componentName, info.flags, info.user);
     }
 
     /**
@@ -365,7 +105,7 @@
                         appWidgetHost.deleteAppWidgetId(widget.appWidgetId);
                         return null;
                     }
-                }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
+                }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
             }
         } else {
             return false;
@@ -378,18 +118,14 @@
         return true;
     }
 
-    public void onDrop(DragObject d) {
-        animateToTrashAndCompleteDrop(d);
-    }
-
     /**
      * Creates an animation from the current drag view to the delete trash icon.
      */
     private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer,
             DragObject d, PointF vel, ViewConfiguration config) {
 
-        int width = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicWidth();
-        int height = mCurrentDrawable == null ? 0 : mCurrentDrawable.getIntrinsicHeight();
+        int width = mDrawable.getIntrinsicWidth();
+        int height = mDrawable.getIntrinsicHeight();
         final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
                 width, height);
         final Rect from = new Rect();
@@ -502,13 +238,14 @@
     }
 
     public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
-        final boolean isAllApps = d.dragSource instanceof AppsCustomizePagedView;
+        final boolean isWidgets = d.dragSource instanceof WidgetsContainerView;
+        final boolean isAllapps = d.dragSource instanceof AppsContainerView;
 
         // Don't highlight the icon as it's animating
         d.dragView.setColor(0);
         d.dragView.updateInitialScaleToCurrentScale();
         // Don't highlight the target if we are flinging from AllApps
-        if (isAllApps) {
+        if (isWidgets || isAllapps) {
             resetHoverColor();
         }
 
@@ -551,14 +288,13 @@
             updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime,
                     duration, config);
         }
-        deferCompleteDropIfUninstalling(d);
 
         Runnable onAnimationEndRunnable = new Runnable() {
             @Override
             public void run() {
                 // If we are dragging from AllApps, then we allow AppsCustomizePagedView to clean up
                 // itself, otherwise, complete the drop to initiate the deletion process
-                if (!isAllApps) {
+                if (!isWidgets || !isAllapps) {
                     mLauncher.exitSpringLoadedDragMode();
                     completeDrop(d);
                 }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 34e1f3c..22fb6a0 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -38,6 +38,8 @@
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.util.Thunk;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -80,7 +82,8 @@
     boolean isLandscape;
     boolean isTablet;
     boolean isLargeTablet;
-    boolean isLayoutRtl;
+    public boolean isLayoutRtl;
+
     boolean transposeLayoutWithOrientation;
 
     int desiredWorkspaceLeftRightMarginPx;
@@ -103,8 +106,8 @@
     public int cellWidthPx;
     public int cellHeightPx;
 
-    int iconSizePx;
-    int iconTextSizePx;
+    public int iconSizePx;
+    public int iconTextSizePx;
     int iconDrawablePaddingPx;
     int allAppsIconSizePx;
     int allAppsIconTextSizePx;
@@ -122,6 +125,7 @@
     int hotseatAllAppsRank;
     int allAppsNumRows;
     int allAppsNumCols;
+    int appsViewNumCols;
     int searchBarSpaceWidthPx;
     int searchBarSpaceHeightPx;
     int pageIndicatorHeightPx;
@@ -137,7 +141,7 @@
     DeviceProfile(String n, float w, float h, float r, float c,
                   float is, float its, float hs, float his, int dlId) {
         // Ensure that we have an odd number of hotseat items (since we need to place all apps)
-        if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) {
+        if (hs % 2 == 0) {
             throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces");
         }
 
@@ -363,7 +367,7 @@
         }
     }
 
-    private void updateIconSize(float scale, int drawablePadding, Resources resources,
+    private void updateIconSize(float scale, int drawablePadding, Resources res,
                                 DisplayMetrics dm) {
         iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale);
         iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale);
@@ -372,9 +376,9 @@
 
         // Search Bar
         searchBarSpaceWidthPx = Math.min(widthPx,
-                resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
+                res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
         searchBarSpaceHeightPx = getSearchBarTopOffset()
-                + resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
+                + res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
 
         // Calculate the actual text height
         Paint textPaint = new Paint();
@@ -382,7 +386,7 @@
         FontMetrics fm = textPaint.getFontMetrics();
         cellWidthPx = iconSizePx;
         cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top);
-        final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale);
+        final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
         dragViewScale = (iconSizePx + scaleDps) / iconSizePx;
 
         // Hotseat
@@ -400,11 +404,11 @@
         allAppsCellWidthPx = allAppsIconSizePx;
         allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
         int maxLongEdgeCellCount =
-                resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
+                res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
         int maxShortEdgeCellCount =
-                resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
+                res.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
         int minEdgeCellCount =
-                resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
+                res.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
         int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount);
         int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount);
 
@@ -415,10 +419,25 @@
             allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) /
                     (allAppsCellHeightPx + allAppsCellPaddingPx);
             allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows));
-            allAppsNumCols = (availableWidthPx) /
-                    (allAppsCellWidthPx + allAppsCellPaddingPx);
+            allAppsNumCols = (availableWidthPx) / (allAppsCellWidthPx + allAppsCellPaddingPx);
             allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols));
         }
+
+        int appsContainerViewWidthPx = res.getDimensionPixelSize(R.dimen.apps_container_width);
+        updateAppsViewNumCols(res, appsContainerViewWidthPx);
+    }
+
+    public boolean updateAppsViewNumCols(Resources res, int containerWidth) {
+        int appsViewLeftMarginPx =
+                res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
+        int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx;
+        int numCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
+                (allAppsCellWidthPx + 2 * allAppsCellPaddingPx);
+        if (numCols != appsViewNumCols) {
+            appsViewNumCols = numCols;
+            return true;
+        }
+        return false;
     }
 
     void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx,
@@ -440,7 +459,7 @@
         updateAvailableDimensions(context);
     }
 
-    private float dist(PointF p0, PointF p1) {
+    @Thunk float dist(PointF p0, PointF p1) {
         return (float) Math.sqrt((p1.x - p0.x)*(p1.x-p0.x) +
                 (p1.y-p0.y)*(p1.y-p0.y));
     }
@@ -691,6 +710,10 @@
         return isLargeTablet;
     }
 
+    /**
+     * When {@code true}, hotseat is on the bottom row when in landscape mode.
+     * If {@code false}, hotseat is on the right column when in landscape mode.
+     */
     boolean isVerticalBarLayout() {
         return isLandscape && transposeLayoutWithOrientation;
     }
@@ -788,64 +811,6 @@
             }
         }
 
-        // Layout AllApps
-        AppsCustomizeTabHost host = (AppsCustomizeTabHost)
-                launcher.findViewById(R.id.apps_customize_pane);
-        if (host != null) {
-            // Center the all apps page indicator
-            int pageIndicatorHeight = (int) (pageIndicatorHeightPx * Math.min(1f,
-                    (allAppsIconSizePx / DynamicGrid.DEFAULT_ICON_SIZE_PX)));
-            pageIndicator = host.findViewById(R.id.apps_customize_page_indicator);
-            if (pageIndicator != null) {
-                LinearLayout.LayoutParams lllp = (LinearLayout.LayoutParams) pageIndicator.getLayoutParams();
-                lllp.width = LayoutParams.WRAP_CONTENT;
-                lllp.height = pageIndicatorHeight;
-                pageIndicator.setLayoutParams(lllp);
-            }
-
-            AppsCustomizePagedView pagedView = (AppsCustomizePagedView)
-                    host.findViewById(R.id.apps_customize_pane_content);
-
-            FrameLayout fakePageContainer = (FrameLayout)
-                    host.findViewById(R.id.fake_page_container);
-            FrameLayout fakePage = (FrameLayout) host.findViewById(R.id.fake_page);
-
-            padding = new Rect();
-            if (pagedView != null) {
-                // Constrain the dimensions of all apps so that it does not span the full width
-                int paddingLR = (availableWidthPx - (allAppsCellWidthPx * allAppsNumCols)) /
-                        (2 * (allAppsNumCols + 1));
-                int paddingTB = (availableHeightPx - (allAppsCellHeightPx * allAppsNumRows)) /
-                        (2 * (allAppsNumRows + 1));
-                paddingLR = Math.min(paddingLR, (int)((paddingLR + paddingTB) * 0.75f));
-                paddingTB = Math.min(paddingTB, (int)((paddingLR + paddingTB) * 0.75f));
-                int maxAllAppsWidth = (allAppsNumCols * (allAppsCellWidthPx + 2 * paddingLR));
-                int gridPaddingLR = (availableWidthPx - maxAllAppsWidth) / 2;
-                // Only adjust the side paddings on landscape phones, or tablets
-                if ((isTablet() || isLandscape) && gridPaddingLR > (allAppsCellWidthPx / 4)) {
-                    padding.left = padding.right = gridPaddingLR;
-                }
-
-                // The icons are centered, so we can't just offset by the page indicator height
-                // because the empty space will actually be pageIndicatorHeight + paddingTB
-                padding.bottom = Math.max(0, pageIndicatorHeight - paddingTB);
-
-                pagedView.setWidgetsPageIndicatorPadding(pageIndicatorHeight);
-                fakePage.setBackground(res.getDrawable(R.drawable.quantum_panel));
-
-                // Horizontal padding for the whole paged view
-                int pagedFixedViewPadding =
-                        res.getDimensionPixelSize(R.dimen.apps_customize_horizontal_padding);
-
-                padding.left += pagedFixedViewPadding;
-                padding.right += pagedFixedViewPadding;
-
-                pagedView.setPadding(padding.left, padding.top, padding.right, padding.bottom);
-                fakePageContainer.setPadding(padding.left, padding.top, padding.right, padding.bottom);
-
-            }
-        }
-
         // Layout the Overview Mode
         ViewGroup overviewMode = launcher.getOverviewPanel();
         if (overviewMode != null) {
diff --git a/src/com/android/launcher3/DragController.java b/src/com/android/launcher3/DragController.java
index 480dce9..b24608c 100644
--- a/src/com/android/launcher3/DragController.java
+++ b/src/com/android/launcher3/DragController.java
@@ -34,6 +34,8 @@
 import android.view.ViewConfiguration;
 import android.view.inputmethod.InputMethodManager;
 
+import com.android.launcher3.util.Thunk;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 
@@ -49,8 +51,8 @@
     /** Indicates the drag is a copy.  */
     public static int DRAG_ACTION_COPY = 1;
 
-    private static final int SCROLL_DELAY = 500;
-    private static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150;
+    public static final int SCROLL_DELAY = 500;
+    public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150;
 
     private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
 
@@ -63,7 +65,7 @@
 
     private static final float MAX_FLING_DEGREES = 35f;
 
-    private Launcher mLauncher;
+    @Thunk Launcher mLauncher;
     private Handler mHandler;
 
     // temporaries to avoid gc thrash
@@ -73,6 +75,9 @@
     /** Whether or not we're dragging. */
     private boolean mDragging;
 
+    /** Whether or not this is an accessible drag operation */
+    private boolean mIsAccessibleDrag;
+
     /** X coordinate of the down event. */
     private int mMotionDownX;
 
@@ -99,17 +104,17 @@
 
     private View mMoveTarget;
 
-    private DragScroller mDragScroller;
-    private int mScrollState = SCROLL_OUTSIDE_ZONE;
+    @Thunk DragScroller mDragScroller;
+    @Thunk int mScrollState = SCROLL_OUTSIDE_ZONE;
     private ScrollRunnable mScrollRunnable = new ScrollRunnable();
 
     private DropTarget mLastDropTarget;
 
     private InputMethodManager mInputMethodManager;
 
-    private int mLastTouch[] = new int[2];
-    private long mLastTouchUpTime = -1;
-    private int mDistanceSinceScroll = 0;
+    @Thunk int mLastTouch[] = new int[2];
+    @Thunk long mLastTouchUpTime = -1;
+    @Thunk int mDistanceSinceScroll = 0;
 
     private int mTmpPoint[] = new int[2];
     private Rect mDragLayerRect = new Rect();
@@ -120,7 +125,7 @@
     /**
      * Interface to receive notifications when a drag starts or stops
      */
-    interface DragListener {
+    public interface DragListener {
         /**
          * A drag has begun
          *
@@ -182,7 +187,7 @@
                 (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2);
 
         startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null,
-                null, initialDragViewScale);
+                null, initialDragViewScale, false);
 
         if (dragAction == DRAG_ACTION_MOVE) {
             v.setVisibility(View.GONE);
@@ -202,10 +207,11 @@
      *        {@link #DRAG_ACTION_COPY}
      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
      *          Makes dragging feel more precise, e.g. you can clip out a transparent border
+     * @param accessible whether this drag should occur in accessibility mode
      */
     public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
             DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
-            float initialDragViewScale) {
+            float initialDragViewScale, boolean accessible) {
         if (PROFILE_DRAWING_DURING_DRAG) {
             android.os.Debug.startMethodTracing("Launcher");
         }
@@ -228,12 +234,21 @@
         final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
 
         mDragging = true;
+        mIsAccessibleDrag = accessible;
 
         mDragObject = new DropTarget.DragObject();
 
         mDragObject.dragComplete = false;
-        mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
-        mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
+        if (mIsAccessibleDrag) {
+            // For an accessible drag, we assume the view is being dragged from the center.
+            mDragObject.xOffset = b.getWidth() / 2;
+            mDragObject.yOffset = b.getHeight() / 2;
+            mDragObject.accessibleDrag = true;
+        } else {
+            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
+            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
+        }
+
         mDragObject.dragSource = source;
         mDragObject.dragInfo = dragInfo;
 
@@ -349,6 +364,7 @@
     private void endDrag() {
         if (mDragging) {
             mDragging = false;
+            mIsAccessibleDrag = false;
             clearScrollRunnable();
             boolean isDeferred = false;
             if (mDragObject.dragView != null) {
@@ -361,7 +377,7 @@
 
             // Only end the drag if we are not deferred
             if (!isDeferred) {
-                for (DragListener listener : mListeners) {
+                for (DragListener listener : new ArrayList<>(mListeners)) {
                     listener.onDragEnd();
                 }
             }
@@ -378,13 +394,13 @@
 
         if (mDragObject.deferDragViewCleanupPostAnimation) {
             // If we skipped calling onDragEnd() before, do it now
-            for (DragListener listener : mListeners) {
+            for (DragListener listener : new ArrayList<>(mListeners)) {
                 listener.onDragEnd();
             }
         }
     }
 
-    void onDeferredEndFling(DropTarget.DragObject d) {
+    public void onDeferredEndFling(DropTarget.DragObject d) {
         d.dragSource.onFlingToDeleteCompleted();
     }
 
@@ -421,6 +437,10 @@
                     + mDragging);
         }
 
+        if (mIsAccessibleDrag) {
+            return false;
+        }
+
         // Update the velocity tracker
         acquireVelocityTrackerAndAddMovement(ev);
 
@@ -442,7 +462,8 @@
                 mLastTouchUpTime = System.currentTimeMillis();
                 if (mDragging) {
                     PointF vec = isFlingingToDelete(mDragObject.dragSource);
-                    if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) {
+                    if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragSource,
+                            mDragObject.dragInfo)) {
                         vec = null;
                     }
                     if (vec != null) {
@@ -525,7 +546,7 @@
         mLastDropTarget = dropTarget;
     }
 
-    private void checkScrollState(int x, int y) {
+    @Thunk void checkScrollState(int x, int y) {
         final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
         final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY;
         final DragLayer dragLayer = mLauncher.getDragLayer();
@@ -560,7 +581,7 @@
      * Call this from a drag source view.
      */
     public boolean onTouchEvent(MotionEvent ev) {
-        if (!mDragging) {
+        if (!mDragging || mIsAccessibleDrag) {
             return false;
         }
 
@@ -596,7 +617,7 @@
 
             if (mDragging) {
                 PointF vec = isFlingingToDelete(mDragObject.dragSource);
-                if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragInfo)) {
+                if (!DeleteDropTarget.willAcceptDrop(mDragObject.dragSource, mDragObject.dragInfo)) {
                     vec = null;
                 }
                 if (vec != null) {
@@ -617,6 +638,34 @@
     }
 
     /**
+     * Since accessible drag and drop won't cause the same sequence of touch events, we manually
+     * inject the appropriate state.
+     */
+    public void prepareAccessibleDrag(int x, int y) {
+        mMotionDownX = x;
+        mMotionDownY = y;
+        mLastDropTarget = null;
+    }
+
+    /**
+     * As above, since accessible drag and drop won't cause the same sequence of touch events,
+     * we manually ensure appropriate drag and drop events get emulated for accessible drag.
+     */
+    public void completeAccessibleDrag(int[] location) {
+        final int[] coordinates = mCoordinatesTemp;
+
+        // We make sure that we prime the target for drop.
+        DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates);
+        mDragObject.x = coordinates[0];
+        mDragObject.y = coordinates[1];
+        checkTouchMove(dropTarget);
+
+        // Perform the drop
+        drop(location[0], location[1]);
+        endDrag();
+    }
+
+    /**
      * Determines whether the user flung the current item to delete it.
      *
      * @return the vector at which the item was flung, or null if no fling was detected.
diff --git a/src/com/android/launcher3/DragLayer.java b/src/com/android/launcher3/DragLayer.java
index a352b79..ab2e094 100644
--- a/src/com/android/launcher3/DragLayer.java
+++ b/src/com/android/launcher3/DragLayer.java
@@ -39,6 +39,7 @@
 import android.widget.TextView;
 
 import com.android.launcher3.InsettableFrameLayout.LayoutParams;
+import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 
@@ -46,7 +47,7 @@
  * A ViewGroup that coordinates dragging across its descendants
  */
 public class DragLayer extends InsettableFrameLayout {
-    private DragController mDragController;
+    @Thunk DragController mDragController;
     private int[] mTmpXY = new int[2];
 
     private int mXDown, mYDown;
@@ -61,9 +62,9 @@
     private ValueAnimator mDropAnim = null;
     private ValueAnimator mFadeOutAnim = null;
     private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
-    private DragView mDropView = null;
-    private int mAnchorViewInitialScrollX = 0;
-    private View mAnchorView = null;
+    @Thunk DragView mDropView = null;
+    @Thunk int mAnchorViewInitialScrollX = 0;
+    @Thunk View mAnchorView = null;
 
     private boolean mHoverPointClosesFolder = false;
     private Rect mHitRect = new Rect();
@@ -779,7 +780,7 @@
         return mDropView;
     }
 
-    private void fadeOutDragView() {
+    @Thunk void fadeOutDragView() {
         mFadeOutAnim = new ValueAnimator();
         mFadeOutAnim.setDuration(150);
         mFadeOutAnim.setFloatValues(0f, 1f);
diff --git a/src/com/android/launcher3/DragSource.java b/src/com/android/launcher3/DragSource.java
index 7369eea..2a1346e 100644
--- a/src/com/android/launcher3/DragSource.java
+++ b/src/com/android/launcher3/DragSource.java
@@ -22,9 +22,9 @@
 
 /**
  * Interface defining an object that can originate a drag.
- *
  */
 public interface DragSource {
+
     /**
      * @return whether items dragged from this source supports
      */
diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java
index ea34e46..b1a6266 100644
--- a/src/com/android/launcher3/DragView.java
+++ b/src/com/android/launcher3/DragView.java
@@ -29,8 +29,10 @@
 import android.view.View;
 import android.view.animation.DecelerateInterpolator;
 
+import com.android.launcher3.util.Thunk;
+
 public class DragView extends View {
-    private static float sDragAlpha = 1f;
+    @Thunk static float sDragAlpha = 1f;
 
     private Bitmap mBitmap;
     private Bitmap mCrossFadeBitmap;
@@ -42,11 +44,11 @@
     private Rect mDragRegion = null;
     private DragLayer mDragLayer = null;
     private boolean mHasDrawn = false;
-    private float mCrossFadeProgress = 0f;
+    @Thunk float mCrossFadeProgress = 0f;
 
     ValueAnimator mAnim;
-    private float mOffsetX = 0.0f;
-    private float mOffsetY = 0.0f;
+    @Thunk float mOffsetX = 0.0f;
+    @Thunk float mOffsetY = 0.0f;
     private float mInitialScale = 1f;
     // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace
     // size.  This is ignored for non-icons.
@@ -70,8 +72,6 @@
         mInitialScale = initialScale;
 
         final Resources res = getResources();
-        final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX);
-        final float offsetY = res.getDimensionPixelSize(R.dimen.dragViewOffsetY);
         final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
         final float scale = (width + scaleDps) / width;
 
@@ -87,8 +87,8 @@
             public void onAnimationUpdate(ValueAnimator animation) {
                 final float value = (Float) animation.getAnimatedValue();
 
-                final int deltaX = (int) ((value * offsetX) - mOffsetX);
-                final int deltaY = (int) ((value * offsetY) - mOffsetY);
+                final int deltaX = (int) (-mOffsetX);
+                final int deltaY = (int) (-mOffsetY);
 
                 mOffsetX += deltaX;
                 mOffsetY += deltaY;
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index 64f0ac8..c5cca3b 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -54,6 +54,9 @@
         /** Where the drag originated */
         public DragSource dragSource = null;
 
+        /** The object is part of an accessible drag operation */
+        public boolean accessibleDrag;
+
         /** Post drag animation runnable */
         public Runnable postAnimationRunnable = null;
 
@@ -65,6 +68,29 @@
 
         public DragObject() {
         }
+
+        /**
+         * This is used to compute the visual center of the dragView. This point is then
+         * used to visualize drop locations and determine where to drop an item. The idea is that
+         * the visual center represents the user's interpretation of where the item is, and hence
+         * is the appropriate point to use when determining drop location.
+         */
+        public final float[] getVisualCenter(float[] recycle) {
+            final float res[] = (recycle == null) ? new float[2] : recycle;
+
+            // These represent the visual top and left of drag view if a dragRect was provided.
+            // If a dragRect was not provided, then they correspond to the actual view left and
+            // top, as the dragRect is in that case taken to be the entire dragView.
+            // R.dimen.dragViewOffsetY.
+            int left = x - xOffset;
+            int top = y - yOffset;
+
+            // In order to find the visual center, we shift by half the dragRect
+            res[0] = left + dragView.getDragRegion().width() / 2;
+            res[1] = top + dragView.getDragRegion().height() / 2;
+
+            return res;
+        }
     }
 
     public static class DragEnforcer implements DragController.DragListener {
@@ -113,7 +139,7 @@
 
     /**
      * Handle an object being dropped on the DropTarget
-     * 
+     *
      * @param source DragSource where the drag started
      * @param x X coordinate of the drop location
      * @param y Y coordinate of the drop location
@@ -143,7 +169,7 @@
     /**
      * Check if a drop action can occur at, or near, the requested location.
      * This will be called just before onDrop.
-     * 
+     *
      * @param source DragSource where the drag started
      * @param x X coordinate of the drop location
      * @param y Y coordinate of the drop location
diff --git a/src/com/android/launcher3/DynamicGrid.java b/src/com/android/launcher3/DynamicGrid.java
index aa08148..24da97f 100644
--- a/src/com/android/launcher3/DynamicGrid.java
+++ b/src/com/android/launcher3/DynamicGrid.java
@@ -56,23 +56,22 @@
         DisplayMetrics dm = resources.getDisplayMetrics();
         ArrayList<DeviceProfile> deviceProfiles =
                 new ArrayList<DeviceProfile>();
-        boolean hasAA = !LauncherAppState.isDisableAllApps();
         DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm);
         // Our phone profiles include the bar sizes in each orientation
         deviceProfiles.add(new DeviceProfile("Super Short Stubby",
-                255, 300,  2, 3,  48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4));
+                255, 300,  2, 3,  48, 13, 3, 48, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Shorter Stubby",
-                255, 400,  3, 3,  48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4));
+                255, 400,  3, 3,  48, 13, 3, 48, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Short Stubby",
-                275, 420,  3, 4,  48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4));
+                275, 420,  3, 4,  48, 13, 5, 48, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Stubby",
-                255, 450,  3, 4,  48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4));
+                255, 450,  3, 4,  48, 13, 5, 48, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Nexus S",
-                296, 491.33f,  4, 4,  48, 13, (hasAA ? 5 : 5), 48, R.xml.default_workspace_4x4));
+                296, 491.33f,  4, 4,  48, 13, 5, 48, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Nexus 4",
-                335, 567,  4, 4,  DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4));
+                335, 567,  4, 4,  DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Nexus 5",
-                359, 567,  4, 4,  DEFAULT_ICON_SIZE_DP, 13, (hasAA ? 5 : 5), 56, R.xml.default_workspace_4x4));
+                359, 567,  4, 4,  DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
         deviceProfiles.add(new DeviceProfile("Large Phone",
                 406, 694,  5, 5,  64, 14.4f,  5, 56, R.xml.default_workspace_5x5));
         // The tablet profile is odd in that the landscape orientation
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index ff02bbb..28e923e 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -32,7 +32,7 @@
 import android.graphics.drawable.Drawable;
 import android.util.SparseArray;
 
-class FastBitmapDrawable extends Drawable {
+public class FastBitmapDrawable extends Drawable {
 
     static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
 
@@ -72,7 +72,7 @@
     private boolean mPressed = false;
     private ObjectAnimator mPressedAnimator;
 
-    FastBitmapDrawable(Bitmap b) {
+    public FastBitmapDrawable(Bitmap b) {
         mAlpha = 255;
         mBitmap = b;
         setBounds(0, 0, b.getWidth(), b.getHeight());
diff --git a/src/com/android/launcher3/FirstFrameAnimatorHelper.java b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
index 095c563..a51ddd4 100644
--- a/src/com/android/launcher3/FirstFrameAnimatorHelper.java
+++ b/src/com/android/launcher3/FirstFrameAnimatorHelper.java
@@ -24,6 +24,8 @@
 import android.view.ViewPropertyAnimator;
 import android.view.ViewTreeObserver;
 
+import com.android.launcher3.util.Thunk;
+
 /*
  *  This is a helper class that listens to updates from the corresponding animation.
  *  For the first two frames, it adjusts the current play time of the animation to
@@ -41,7 +43,7 @@
     private boolean mAdjustedSecondFrameTime;
 
     private static ViewTreeObserver.OnDrawListener sGlobalDrawListener;
-    private static long sGlobalFrameCounter;
+    @Thunk static long sGlobalFrameCounter;
     private static boolean sVisible;
 
     public FirstFrameAnimatorHelper(ValueAnimator animator, View target) {
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index e607047..c77d416 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011 The Android Open Source Project
+ * 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.
@@ -16,296 +16,407 @@
 
 package com.android.launcher3;
 
-import android.content.res.Configuration;
+import android.util.Log;
 import android.view.KeyEvent;
 import android.view.SoundEffectConstants;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ScrollView;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
+import com.android.launcher3.util.FocusLogic;
+import com.android.launcher3.util.Thunk;
 
 /**
  * A keyboard listener we set on all the workspace icons.
  */
 class IconKeyEventListener implements View.OnKeyListener {
+    @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
         return FocusHelper.handleIconKeyEvent(v, keyCode, event);
     }
 }
 
 /**
- * A keyboard listener we set on all the workspace icons.
- */
-class FolderKeyEventListener implements View.OnKeyListener {
-    public boolean onKey(View v, int keyCode, KeyEvent event) {
-        return FocusHelper.handleFolderKeyEvent(v, keyCode, event);
-    }
-}
-
-/**
  * A keyboard listener we set on all the hotseat buttons.
  */
 class HotseatIconKeyEventListener implements View.OnKeyListener {
+    @Override
     public boolean onKey(View v, int keyCode, KeyEvent event) {
-        final Configuration configuration = v.getResources().getConfiguration();
-        return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event, configuration.orientation);
+        return FocusHelper.handleHotseatButtonKeyEvent(v, keyCode, event);
     }
 }
 
 public class FocusHelper {
 
+    private static final String TAG = "FocusHelper";
+    private static final boolean DEBUG = false;
+
     /**
-     * Returns the Viewgroup containing page contents for the page at the index specified.
+     * Handles key events in paged folder.
      */
-    private static ViewGroup getAppsCustomizePage(ViewGroup container, int index) {
-        ViewGroup page = (ViewGroup) ((PagedView) container).getPageAt(index);
-        if (page instanceof CellLayout) {
-            // There are two layers, a PagedViewCellLayout and PagedViewCellLayoutChildren
-            page = ((CellLayout) page).getShortcutsAndWidgets();
+    public static class PagedFolderKeyEventListener implements View.OnKeyListener {
+
+        private final Folder mFolder;
+
+        public PagedFolderKeyEventListener(Folder folder) {
+            mFolder = folder;
         }
-        return page;
+
+        @Override
+        public boolean onKey(View v, int keyCode, KeyEvent e) {
+            boolean consume = FocusLogic.shouldConsume(keyCode);
+            if (e.getAction() == KeyEvent.ACTION_UP) {
+                return consume;
+            }
+            if (DEBUG) {
+                Log.v(TAG, String.format("Handle ALL Folders keyevent=[%s].",
+                        KeyEvent.keyCodeToString(keyCode)));
+            }
+
+
+            if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
+                if (LauncherAppState.isDogfoodBuild()) {
+                    throw new IllegalStateException("Parent of the focused item is not supported.");
+                } else {
+                    return false;
+                }
+            }
+
+            // Initialize variables.
+            final ShortcutAndWidgetContainer itemContainer = (ShortcutAndWidgetContainer) v.getParent();
+            final CellLayout cellLayout = (CellLayout) itemContainer.getParent();
+            final int countX = cellLayout.getCountX();
+            final int countY = cellLayout.getCountY();
+
+            final int iconIndex = itemContainer.indexOfChild(v);
+            final FolderPagedView pagedView = (FolderPagedView) cellLayout.getParent();
+
+            final int pageIndex = pagedView.indexOfChild(cellLayout);
+            final int pageCount = pagedView.getPageCount();
+
+            int[][] matrix = FocusLogic.createSparseMatrix(cellLayout);
+            // Process focus.
+            int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
+                    iconIndex, pageIndex, pageCount);
+            if (newIconIndex == FocusLogic.NOOP) {
+                handleNoopKey(keyCode, v);
+                return consume;
+            }
+            ShortcutAndWidgetContainer newParent = null;
+            View child = null;
+
+            switch (newIconIndex) {
+                case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
+                case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
+                    newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
+                    if (newParent != null) {
+                        int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
+                        pagedView.snapToPage(pageIndex - 1);
+                        child = newParent.getChildAt(
+                                ((newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN)
+                                    ^ newParent.invertLayoutHorizontally()) ? 0 : countX - 1, row);
+                    }
+                    break;
+                case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
+                    newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
+                    if (newParent != null) {
+                        pagedView.snapToPage(pageIndex - 1);
+                        child = newParent.getChildAt(0, 0);
+                    }
+                    break;
+                case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
+                    newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex - 1);
+                    if (newParent != null) {
+                        pagedView.snapToPage(pageIndex - 1);
+                        child = newParent.getChildAt(countX - 1, countY - 1);
+                    }
+                    break;
+                case FocusLogic.NEXT_PAGE_FIRST_ITEM:
+                    newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
+                    if (newParent != null) {
+                        pagedView.snapToPage(pageIndex + 1);
+                        child = newParent.getChildAt(0, 0);
+                    }
+                    break;
+                case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
+                case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
+                    newParent = getCellLayoutChildrenForIndex(pagedView, pageIndex + 1);
+                    if (newParent != null) {
+                        pagedView.snapToPage(pageIndex + 1);
+                        child = FocusLogic.getAdjacentChildInNextPage(newParent, v, newIconIndex);
+                    }
+                    break;
+                case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
+                    child = cellLayout.getChildAt(0, 0);
+                    break;
+                case FocusLogic.CURRENT_PAGE_LAST_ITEM:
+                    child = pagedView.getLastItem();
+                    break;
+                default: // Go to some item on the current page.
+                    child = itemContainer.getChildAt(newIconIndex);
+                    break;
+            }
+            if (child != null) {
+                child.requestFocus();
+                playSoundEffect(keyCode, v);
+            } else {
+                handleNoopKey(keyCode, v);
+            }
+            return consume;
+        }
+
+        public void handleNoopKey(int keyCode, View v) {
+            if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+                mFolder.mFolderName.requestFocus();
+                playSoundEffect(keyCode, v);
+            }
+        }
     }
 
     /**
-     * Handles key events in a PageViewCellLayout containing PagedViewIcons.
+     * Handles key events in the workspace hot seat (bottom of the screen).
+     * <p>Currently we don't special case for the phone UI in different orientations, even though
+     * the hotseat is on the side in landscape mode. This is to ensure that accessibility
+     * consistency is maintained across rotations.
      */
-    static boolean handleAppsCustomizeKeyEvent(View v, int keyCode, KeyEvent e) {
-        ViewGroup parentLayout;
-        ViewGroup itemContainer;
-        int countX;
-        int countY;
-        if (v.getParent() instanceof ShortcutAndWidgetContainer) {
-            itemContainer = (ViewGroup) v.getParent();
-            parentLayout = (ViewGroup) itemContainer.getParent();
-            countX = ((CellLayout) parentLayout).getCountX();
-            countY = ((CellLayout) parentLayout).getCountY();
-        } else {
-            itemContainer = parentLayout = (ViewGroup) v.getParent();
-            countX = ((PagedViewGridLayout) parentLayout).getCellCountX();
-            countY = ((PagedViewGridLayout) parentLayout).getCellCountY();
+    static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e) {
+        boolean consume = FocusLogic.shouldConsume(keyCode);
+        if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
+            return consume;
         }
 
-        // Note we have an extra parent because of the
-        // PagedViewCellLayout/PagedViewCellLayoutChildren relationship
-        final PagedView container = (PagedView) parentLayout.getParent();
-        final int iconIndex = itemContainer.indexOfChild(v);
-        final int itemCount = itemContainer.getChildCount();
-        final int pageIndex = ((PagedView) container).indexToPage(container.indexOfChild(parentLayout));
-        final int pageCount = container.getChildCount();
-
-        final int x = iconIndex % countX;
-        final int y = iconIndex / countX;
-
-        final int action = e.getAction();
-        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
-        ViewGroup newParent = null;
-        // Side pages do not always load synchronously, so check before focusing child siblings
-        // willy-nilly
-        View child = null;
-        boolean wasHandled = false;
-        switch (keyCode) {
-            case KeyEvent.KEYCODE_DPAD_LEFT:
-                if (handleKeyEvent) {
-                    // Select the previous icon or the last icon on the previous page
-                    if (iconIndex > 0) {
-                        itemContainer.getChildAt(iconIndex - 1).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
-                    } else {
-                        if (pageIndex > 0) {
-                            newParent = getAppsCustomizePage(container, pageIndex - 1);
-                            if (newParent != null) {
-                                container.snapToPage(pageIndex - 1);
-                                child = newParent.getChildAt(newParent.getChildCount() - 1);
-                                if (child != null) {
-                                    child.requestFocus();
-                                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
-                                }
-                            }
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_RIGHT:
-                if (handleKeyEvent) {
-                    // Select the next icon or the first icon on the next page
-                    if (iconIndex < (itemCount - 1)) {
-                        itemContainer.getChildAt(iconIndex + 1).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
-                    } else {
-                        if (pageIndex < (pageCount - 1)) {
-                            newParent = getAppsCustomizePage(container, pageIndex + 1);
-                            if (newParent != null) {
-                                container.snapToPage(pageIndex + 1);
-                                child = newParent.getChildAt(0);
-                                if (child != null) {
-                                    child.requestFocus();
-                                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
-                                }
-                            }
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_UP:
-                if (handleKeyEvent) {
-                    // Select the closest icon in the previous row, otherwise select the tab bar
-                    if (y > 0) {
-                        int newiconIndex = ((y - 1) * countX) + x;
-                        itemContainer.getChildAt(newiconIndex).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_DOWN:
-                if (handleKeyEvent) {
-                    // Select the closest icon in the next row, otherwise do nothing
-                    if (y < (countY - 1)) {
-                        int newiconIndex = Math.min(itemCount - 1, ((y + 1) * countX) + x);
-                        int newIconY = newiconIndex / countX;
-                        if (newIconY != y) {
-                            itemContainer.getChildAt(newiconIndex).requestFocus();
-                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_PAGE_UP:
-                if (handleKeyEvent) {
-                    // Select the first icon on the previous page, or the first icon on this page
-                    // if there is no previous page
-                    if (pageIndex > 0) {
-                        newParent = getAppsCustomizePage(container, pageIndex - 1);
-                        if (newParent != null) {
-                            container.snapToPage(pageIndex - 1);
-                            child = newParent.getChildAt(0);
-                            if (child != null) {
-                                child.requestFocus();
-                                v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                            }
-                        }
-                    } else {
-                        itemContainer.getChildAt(0).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_PAGE_DOWN:
-                if (handleKeyEvent) {
-                    // Select the first icon on the next page, or the last icon on this page
-                    // if there is no next page
-                    if (pageIndex < (pageCount - 1)) {
-                        newParent = getAppsCustomizePage(container, pageIndex + 1);
-                        if (newParent != null) {
-                            container.snapToPage(pageIndex + 1);
-                            child = newParent.getChildAt(0);
-                            if (child != null) {
-                                child.requestFocus();
-                                v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                            }
-                        }
-                    } else {
-                        itemContainer.getChildAt(itemCount - 1).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_MOVE_HOME:
-                if (handleKeyEvent) {
-                    // Select the first icon on this page
-                    itemContainer.getChildAt(0).requestFocus();
-                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_MOVE_END:
-                if (handleKeyEvent) {
-                    // Select the last icon on this page
-                    itemContainer.getChildAt(itemCount - 1).requestFocus();
-                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                }
-                wasHandled = true;
-                break;
-            default: break;
+        DeviceProfile profile = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
+        if (DEBUG) {
+            Log.v(TAG, String.format(
+                    "Handle HOTSEAT BUTTONS keyevent=[%s] on hotseat buttons, isVertical=%s",
+                    KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
         }
-        return wasHandled;
+
+        // Initialize the variables.
+        final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
+        final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
+        Hotseat hotseat = (Hotseat) hotseatLayout.getParent();
+
+        Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
+        int pageIndex = workspace.getNextPage();
+        int pageCount = workspace.getChildCount();
+        int countX = -1;
+        int countY = -1;
+        int iconIndex = hotseatParent.indexOfChild(v);
+        int iconRank = ((CellLayout.LayoutParams) hotseatLayout.getShortcutsAndWidgets()
+                .getChildAt(iconIndex).getLayoutParams()).cellX;
+
+        final CellLayout iconLayout = (CellLayout) workspace.getChildAt(pageIndex);
+        if (iconLayout == null) {
+            // This check is to guard against cases where key strokes rushes in when workspace
+            // child creation/deletion is still in flux. (e.g., during drop or fling
+            // animation.)
+            return consume;
+        }
+        final ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
+
+        ViewGroup parent = null;
+        int[][] matrix = null;
+
+        if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
+                !profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
+                    true /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
+                    iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
+            iconIndex += iconParent.getChildCount();
+            countX = iconLayout.getCountX();
+            countY = iconLayout.getCountY() + hotseatLayout.getCountY();
+            parent = iconParent;
+        } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
+                profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
+                    false /* hotseat horizontal */, hotseat.getAllAppsButtonRank(),
+                    iconRank == hotseat.getAllAppsButtonRank() /* include all apps icon */);
+            iconIndex += iconParent.getChildCount();
+            countX = iconLayout.getCountX() + hotseatLayout.getCountX();
+            countY = iconLayout.getCountY();
+            parent = iconParent;
+        } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
+                profile.isVerticalBarLayout()) {
+            keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
+        }else {
+            // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
+            // matrix extended with hotseat.
+            matrix = FocusLogic.createSparseMatrix(hotseatLayout);
+            countX = hotseatLayout.getCountX();
+            countY = hotseatLayout.getCountY();
+            parent = hotseatParent;
+        }
+
+        // Process the focus.
+        int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
+                iconIndex, pageIndex, pageCount);
+
+        View newIcon = null;
+        if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
+            parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
+            newIcon = parent.getChildAt(0);
+            // TODO(hyunyoungs): handle cases where the child is not an icon but
+            // a folder or a widget.
+            workspace.snapToPage(pageIndex + 1);
+        }
+        if (parent == iconParent && newIconIndex >= iconParent.getChildCount()) {
+            newIconIndex -= iconParent.getChildCount();
+        }
+        if (parent != null) {
+            if (newIcon == null && newIconIndex >=0) {
+                newIcon = parent.getChildAt(newIconIndex);
+            }
+            if (newIcon != null) {
+                newIcon.requestFocus();
+                playSoundEffect(keyCode, v);
+            }
+        }
+        return consume;
     }
 
     /**
-     * Handles key events in the workspace hotseat (bottom of the screen).
+     * Handles key events in a workspace containing icons.
      */
-    static boolean handleHotseatButtonKeyEvent(View v, int keyCode, KeyEvent e, int orientation) {
+    static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
+        boolean consume = FocusLogic.shouldConsume(keyCode);
+        if (e.getAction() == KeyEvent.ACTION_UP || !consume) {
+            return consume;
+        }
+
+        LauncherAppState app = LauncherAppState.getInstance();
+        DeviceProfile profile = app.getDynamicGrid().getDeviceProfile();
+
+        if (DEBUG) {
+            Log.v(TAG, String.format("Handle WORKSPACE ICONS keyevent=[%s] isVerticalBar=%s",
+                    KeyEvent.keyCodeToString(keyCode), profile.isVerticalBarLayout()));
+        }
+
+        // Initialize the variables.
         ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
-        final CellLayout layout = (CellLayout) parent.getParent();
+        CellLayout iconLayout = (CellLayout) parent.getParent();
+        final Workspace workspace = (Workspace) iconLayout.getParent();
+        final ViewGroup launcher = (ViewGroup) workspace.getParent();
+        final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
+        final Hotseat hotseat = (Hotseat) launcher.findViewById(R.id.hotseat);
 
-        // NOTE: currently we don't special case for the phone UI in different
-        // orientations, even though the hotseat is on the side in landscape mode. This
-        // is to ensure that accessibility consistency is maintained across rotations.
-        final int action = e.getAction();
-        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
-        boolean wasHandled = false;
-        switch (keyCode) {
-            case KeyEvent.KEYCODE_DPAD_LEFT:
-                if (handleKeyEvent) {
-                    ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
-                    int myIndex = views.indexOf(v);
-                    // Select the previous button, otherwise do nothing
-                    if (myIndex > 0) {
-                        views.get(myIndex - 1).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_RIGHT:
-                if (handleKeyEvent) {
-                    ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
-                    int myIndex = views.indexOf(v);
-                    // Select the next button, otherwise do nothing
-                    if (myIndex < views.size() - 1) {
-                        views.get(myIndex + 1).requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_UP:
-                if (handleKeyEvent) {
-                    final Workspace workspace = (Workspace)
-                            v.getRootView().findViewById(R.id.workspace);
-                    if (workspace != null) {
-                        int pageIndex = workspace.getCurrentPage();
-                        CellLayout topLayout = (CellLayout) workspace.getChildAt(pageIndex);
-                        ShortcutAndWidgetContainer children = topLayout.getShortcutsAndWidgets();
-                        final View newIcon = getIconInDirection(layout, children, -1, 1);
-                        // Select the first bubble text view in the current page of the workspace
-                        if (newIcon != null) {
-                            newIcon.requestFocus();
-                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                        } else {
-                            workspace.requestFocus();
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_DOWN:
-                // Do nothing
-                wasHandled = true;
-                break;
-            default: break;
+        final int iconIndex = parent.indexOfChild(v);
+        final int pageIndex = workspace.indexOfChild(iconLayout);
+        final int pageCount = workspace.getChildCount();
+        int countX = iconLayout.getCountX();
+        int countY = iconLayout.getCountY();
+
+        CellLayout hotseatLayout = (CellLayout) hotseat.getChildAt(0);
+        ShortcutAndWidgetContainer hotseatParent = hotseatLayout.getShortcutsAndWidgets();
+        int[][] matrix;
+
+        // KEYCODE_DPAD_DOWN in portrait (KEYCODE_DPAD_RIGHT in landscape) is the only key allowed
+        // to take a user to the hotseat. For other dpad navigation, do not use the matrix extended
+        // with the hotseat.
+        if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
+                    hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
+            countY = countY + 1;
+        } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
+                profile.isVerticalBarLayout()) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
+                    hotseat.getAllAppsButtonRank(), false /* all apps icon is ignored */);
+            countX = countX + 1;
+        } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
+            workspace.removeWorkspaceItem(v);
+            return consume;
+        } else {
+            matrix = FocusLogic.createSparseMatrix(iconLayout);
         }
-        return wasHandled;
+
+        // Process the focus.
+        int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX, countY, matrix,
+                iconIndex, pageIndex, pageCount);
+        View newIcon = null;
+        switch (newIconIndex) {
+            case FocusLogic.NOOP:
+                if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
+                    newIcon = tabs;
+                }
+                break;
+            case FocusLogic.PREVIOUS_PAGE_RIGHT_COLUMN:
+            case FocusLogic.NEXT_PAGE_RIGHT_COLUMN:
+                int newPageIndex = pageIndex - 1;
+                if (newIconIndex == FocusLogic.NEXT_PAGE_RIGHT_COLUMN) {
+                    newPageIndex = pageIndex + 1;
+                }
+                int row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
+                parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
+                workspace.snapToPage(newPageIndex);
+                if (parent != null) {
+                    workspace.snapToPage(newPageIndex);
+                    iconLayout = (CellLayout) parent.getParent();
+                    matrix = FocusLogic.createSparseMatrix(iconLayout,
+                        iconLayout.getCountX(), row);
+                    newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix,
+                        FocusLogic.PIVOT, newPageIndex, pageCount);
+                    newIcon = parent.getChildAt(newIconIndex);
+                }
+                break;
+            case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
+                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
+                newIcon = parent.getChildAt(0);
+                workspace.snapToPage(pageIndex - 1);
+                break;
+            case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
+                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
+                newIcon = parent.getChildAt(parent.getChildCount() - 1);
+                workspace.snapToPage(pageIndex - 1);
+                break;
+            case FocusLogic.NEXT_PAGE_FIRST_ITEM:
+                parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
+                newIcon = parent.getChildAt(0);
+                workspace.snapToPage(pageIndex + 1);
+                break;
+            case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
+            case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
+                newPageIndex = pageIndex + 1;
+                if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN) {
+                    newPageIndex = pageIndex - 1;
+                }
+                workspace.snapToPage(newPageIndex);
+                row = ((CellLayout.LayoutParams) v.getLayoutParams()).cellY;
+                parent = getCellLayoutChildrenForIndex(workspace, newPageIndex);
+                if (parent != null) {
+                    workspace.snapToPage(newPageIndex);
+                    iconLayout = (CellLayout) parent.getParent();
+                    matrix = FocusLogic.createSparseMatrix(iconLayout, -1, row);
+                    newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY, matrix,
+                        FocusLogic.PIVOT, newPageIndex, pageCount);
+                    newIcon = parent.getChildAt(newIconIndex);
+                }
+                break;
+            case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
+                newIcon = parent.getChildAt(0);
+                break;
+            case FocusLogic.CURRENT_PAGE_LAST_ITEM:
+                newIcon = parent.getChildAt(parent.getChildCount() - 1);
+                break;
+            default:
+                // current page, some item.
+                if (0 <= newIconIndex && newIconIndex < parent.getChildCount()) {
+                    newIcon = parent.getChildAt(newIconIndex);
+                } else if (parent.getChildCount() <= newIconIndex &&
+                        newIconIndex < parent.getChildCount() + hotseatParent.getChildCount()) {
+                    newIcon = hotseatParent.getChildAt(newIconIndex - parent.getChildCount());
+                }
+                break;
+        }
+        if (newIcon != null) {
+            newIcon.requestFocus();
+            playSoundEffect(keyCode, v);
+        }
+        return consume;
     }
 
+    //
+    // Helper methods.
+    //
+
     /**
      * Private helper method to get the CellLayoutChildren given a CellLayout index.
      */
@@ -316,358 +427,28 @@
     }
 
     /**
-     * Private helper method to sort all the CellLayout children in order of their (x,y) spatially
-     * from top left to bottom right.
+     * Helper method to be used for playing sound effects.
      */
-    private static ArrayList<View> getCellLayoutChildrenSortedSpatially(CellLayout layout,
-            ViewGroup parent) {
-        // First we order each the CellLayout children by their x,y coordinates
-        final int cellCountX = layout.getCountX();
-        final int count = parent.getChildCount();
-        ArrayList<View> views = new ArrayList<View>();
-        for (int j = 0; j < count; ++j) {
-            views.add(parent.getChildAt(j));
-        }
-        Collections.sort(views, new Comparator<View>() {
-            @Override
-            public int compare(View lhs, View rhs) {
-                CellLayout.LayoutParams llp = (CellLayout.LayoutParams) lhs.getLayoutParams();
-                CellLayout.LayoutParams rlp = (CellLayout.LayoutParams) rhs.getLayoutParams();
-                int lvIndex = (llp.cellY * cellCountX) + llp.cellX;
-                int rvIndex = (rlp.cellY * cellCountX) + rlp.cellX;
-                return lvIndex - rvIndex;
-            }
-        });
-        return views;
-    }
-    /**
-     * Private helper method to find the index of the next BubbleTextView or FolderIcon in the 
-     * direction delta.
-     * 
-     * @param delta either -1 or 1 depending on the direction we want to search
-     */
-    private static View findIndexOfIcon(ArrayList<View> views, int i, int delta) {
-        // Then we find the next BubbleTextView offset by delta from i
-        final int count = views.size();
-        int newI = i + delta;
-        while (0 <= newI && newI < count) {
-            View newV = views.get(newI);
-            if (newV instanceof BubbleTextView || newV instanceof FolderIcon) {
-                return newV;
-            }
-            newI += delta;
-        }
-        return null;
-    }
-    private static View getIconInDirection(CellLayout layout, ViewGroup parent, int i,
-            int delta) {
-        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
-        return findIndexOfIcon(views, i, delta);
-    }
-    private static View getIconInDirection(CellLayout layout, ViewGroup parent, View v,
-            int delta) {
-        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
-        return findIndexOfIcon(views, views.indexOf(v), delta);
-    }
-    /**
-     * Private helper method to find the next closest BubbleTextView or FolderIcon in the direction 
-     * delta on the next line.
-     * 
-     * @param delta either -1 or 1 depending on the line and direction we want to search
-     */
-    private static View getClosestIconOnLine(CellLayout layout, ViewGroup parent, View v,
-            int lineDelta) {
-        final ArrayList<View> views = getCellLayoutChildrenSortedSpatially(layout, parent);
-        final CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
-        final int cellCountY = layout.getCountY();
-        final int row = lp.cellY;
-        final int newRow = row + lineDelta;
-        if (0 <= newRow && newRow < cellCountY) {
-            float closestDistance = Float.MAX_VALUE;
-            int closestIndex = -1;
-            int index = views.indexOf(v);
-            int endIndex = (lineDelta < 0) ? -1 : views.size();
-            while (index != endIndex) {
-                View newV = views.get(index);
-                CellLayout.LayoutParams tmpLp = (CellLayout.LayoutParams) newV.getLayoutParams();
-                boolean satisfiesRow = (lineDelta < 0) ? (tmpLp.cellY < row) : (tmpLp.cellY > row);
-                if (satisfiesRow &&
-                        (newV instanceof BubbleTextView || newV instanceof FolderIcon)) {
-                    float tmpDistance = (float) Math.sqrt(Math.pow(tmpLp.cellX - lp.cellX, 2) +
-                            Math.pow(tmpLp.cellY - lp.cellY, 2));
-                    if (tmpDistance < closestDistance) {
-                        closestIndex = index;
-                        closestDistance = tmpDistance;
-                    }
-                }
-                if (index <= endIndex) {
-                    ++index;
-                } else {
-                    --index;
-                }
-            }
-            if (closestIndex > -1) {
-                return views.get(closestIndex);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Handles key events in a Workspace containing.
-     */
-    static boolean handleIconKeyEvent(View v, int keyCode, KeyEvent e) {
-        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
-        final CellLayout layout = (CellLayout) parent.getParent();
-        final Workspace workspace = (Workspace) layout.getParent();
-        final ViewGroup launcher = (ViewGroup) workspace.getParent();
-        final ViewGroup tabs = (ViewGroup) launcher.findViewById(R.id.search_drop_target_bar);
-        final ViewGroup hotseat = (ViewGroup) launcher.findViewById(R.id.hotseat);
-        int pageIndex = workspace.indexOfChild(layout);
-        int pageCount = workspace.getChildCount();
-
-        final int action = e.getAction();
-        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
-        boolean wasHandled = false;
+    @Thunk static void playSoundEffect(int keyCode, View v) {
         switch (keyCode) {
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                if (handleKeyEvent) {
-                    // Select the previous icon or the last icon on the previous page if possible
-                    View newIcon = getIconInDirection(layout, parent, v, -1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
-                    } else {
-                        if (pageIndex > 0) {
-                            parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
-                            newIcon = getIconInDirection(layout, parent,
-                                    parent.getChildCount(), -1);
-                            if (newIcon != null) {
-                                newIcon.requestFocus();
-                            } else {
-                                // Snap to the previous page
-                                workspace.snapToPage(pageIndex - 1);
-                            }
-                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
-                        }
-                    }
-                }
-                wasHandled = true;
+                v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
                 break;
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                if (handleKeyEvent) {
-                    // Select the next icon or the first icon on the next page if possible
-                    View newIcon = getIconInDirection(layout, parent, v, 1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
-                    } else {
-                        if (pageIndex < (pageCount - 1)) {
-                            parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
-                            newIcon = getIconInDirection(layout, parent, -1, 1);
-                            if (newIcon != null) {
-                                newIcon.requestFocus();
-                            } else {
-                                // Snap to the next page
-                                workspace.snapToPage(pageIndex + 1);
-                            }
-                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_UP:
-                if (handleKeyEvent) {
-                    // Select the closest icon in the previous line, otherwise select the tab bar
-                    View newIcon = getClosestIconOnLine(layout, parent, v, -1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        wasHandled = true;
-                    } else {
-                        tabs.requestFocus();
-                    }
-                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                }
+                v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
                 break;
             case KeyEvent.KEYCODE_DPAD_DOWN:
-                if (handleKeyEvent) {
-                    // Select the closest icon in the next line, otherwise select the button bar
-                    View newIcon = getClosestIconOnLine(layout, parent, v, 1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                        wasHandled = true;
-                    } else if (hotseat != null) {
-                        hotseat.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                    }
-                }
-                break;
-            case KeyEvent.KEYCODE_PAGE_UP:
-                if (handleKeyEvent) {
-                    // Select the first icon on the previous page or the first icon on this page
-                    // if there is no previous page
-                    if (pageIndex > 0) {
-                        parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
-                        View newIcon = getIconInDirection(layout, parent, -1, 1);
-                        if (newIcon != null) {
-                            newIcon.requestFocus();
-                        } else {
-                            // Snap to the previous page
-                            workspace.snapToPage(pageIndex - 1);
-                        }
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                    } else {
-                        View newIcon = getIconInDirection(layout, parent, -1, 1);
-                        if (newIcon != null) {
-                            newIcon.requestFocus();
-                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
             case KeyEvent.KEYCODE_PAGE_DOWN:
-                if (handleKeyEvent) {
-                    // Select the first icon on the next page or the last icon on this page
-                    // if there is no previous page
-                    if (pageIndex < (pageCount - 1)) {
-                        parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
-                        View newIcon = getIconInDirection(layout, parent, -1, 1);
-                        if (newIcon != null) {
-                            newIcon.requestFocus();
-                        } else {
-                            // Snap to the next page
-                            workspace.snapToPage(pageIndex + 1);
-                        }
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                    } else {
-                        View newIcon = getIconInDirection(layout, parent,
-                                parent.getChildCount(), -1);
-                        if (newIcon != null) {
-                            newIcon.requestFocus();
-                            v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                        }
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_MOVE_HOME:
-                if (handleKeyEvent) {
-                    // Select the first icon on this page
-                    View newIcon = getIconInDirection(layout, parent, -1, 1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                    }
-                }
-                wasHandled = true;
-                break;
             case KeyEvent.KEYCODE_MOVE_END:
-                if (handleKeyEvent) {
-                    // Select the last icon on this page
-                    View newIcon = getIconInDirection(layout, parent,
-                            parent.getChildCount(), -1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                    }
-                }
-                wasHandled = true;
-                break;
-            default: break;
-        }
-        return wasHandled;
-    }
-
-    /**
-     * Handles key events for items in a Folder.
-     */
-    static boolean handleFolderKeyEvent(View v, int keyCode, KeyEvent e) {
-        ShortcutAndWidgetContainer parent = (ShortcutAndWidgetContainer) v.getParent();
-        final CellLayout layout = (CellLayout) parent.getParent();
-        final ScrollView scrollView = (ScrollView) layout.getParent();
-        final Folder folder = (Folder) scrollView.getParent();
-        View title = folder.mFolderName;
-
-        final int action = e.getAction();
-        final boolean handleKeyEvent = (action != KeyEvent.ACTION_UP);
-        boolean wasHandled = false;
-        switch (keyCode) {
-            case KeyEvent.KEYCODE_DPAD_LEFT:
-                if (handleKeyEvent) {
-                    // Select the previous icon
-                    View newIcon = getIconInDirection(layout, parent, v, -1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_RIGHT:
-                if (handleKeyEvent) {
-                    // Select the next icon
-                    View newIcon = getIconInDirection(layout, parent, v, 1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                    } else {
-                        title.requestFocus();
-                    }
-                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
-                }
-                wasHandled = true;
+                v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
                 break;
             case KeyEvent.KEYCODE_DPAD_UP:
-                if (handleKeyEvent) {
-                    // Select the closest icon in the previous line
-                    View newIcon = getClosestIconOnLine(layout, parent, v, -1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                    }
-                }
-                wasHandled = true;
-                break;
-            case KeyEvent.KEYCODE_DPAD_DOWN:
-                if (handleKeyEvent) {
-                    // Select the closest icon in the next line
-                    View newIcon = getClosestIconOnLine(layout, parent, v, 1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                    } else {
-                        title.requestFocus();
-                    }
-                    v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                }
-                wasHandled = true;
-                break;
+            case KeyEvent.KEYCODE_PAGE_UP:
             case KeyEvent.KEYCODE_MOVE_HOME:
-                if (handleKeyEvent) {
-                    // Select the first icon on this page
-                    View newIcon = getIconInDirection(layout, parent, -1, 1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
-                    }
-                }
-                wasHandled = true;
+                v.playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
                 break;
-            case KeyEvent.KEYCODE_MOVE_END:
-                if (handleKeyEvent) {
-                    // Select the last icon on this page
-                    View newIcon = getIconInDirection(layout, parent,
-                            parent.getChildCount(), -1);
-                    if (newIcon != null) {
-                        newIcon.requestFocus();
-                        v.playSoundEffect(SoundEffectConstants.NAVIGATION_DOWN);
-                    }
-                }
-                wasHandled = true;
+            default:
                 break;
-            default: break;
         }
-        return wasHandled;
     }
 }
diff --git a/src/com/android/launcher3/FocusIndicatorView.java b/src/com/android/launcher3/FocusIndicatorView.java
index 7d4664a..ecf93e4 100644
--- a/src/com/android/launcher3/FocusIndicatorView.java
+++ b/src/com/android/launcher3/FocusIndicatorView.java
@@ -23,7 +23,8 @@
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.view.View;
-import android.view.ViewParent;
+
+import com.android.launcher3.util.Thunk;
 
 public class FocusIndicatorView extends View implements View.OnFocusChangeListener {
 
@@ -32,9 +33,6 @@
     private static final float MIN_VISIBLE_ALPHA = 0.2f;
     private static final long ANIM_DURATION = 150;
 
-    private static final int[] sTempPos = new int[2];
-    private static final int[] sTempShift = new int[2];
-
     private final int[] mIndicatorPos = new int[2];
     private final int[] mTargetViewPos = new int[2];
 
@@ -80,7 +78,8 @@
         }
 
         if (!mInitiated) {
-            getLocationRelativeToParentPagedView(this, mIndicatorPos);
+            // The parent view should always the a parent of the target view.
+            computeLocationRelativeToParent(this, (View) getParent(), mIndicatorPos);
             mInitiated = true;
         }
 
@@ -93,7 +92,7 @@
             nextState.scaleX = v.getScaleX() * v.getWidth() / indicatorWidth;
             nextState.scaleY = v.getScaleY() * v.getHeight() / indicatorHeight;
 
-            getLocationRelativeToParentPagedView(v, mTargetViewPos);
+            computeLocationRelativeToParent(v, (View) getParent(), mTargetViewPos);
             nextState.x = mTargetViewPos[0] - mIndicatorPos[0] - (1 - nextState.scaleX) * indicatorWidth / 2;
             nextState.y = mTargetViewPos[1] - mIndicatorPos[1] - (1 - nextState.scaleY) * indicatorHeight / 2;
 
@@ -150,31 +149,36 @@
     }
 
     /**
-     * Gets the location of a view relative in the window, off-setting any shift due to
-     * page view scroll
+     * Computes the location of a view relative to {@param parent}, off-setting
+     * any shift due to page view scroll.
+     * @param pos an array of two integers in which to hold the coordinates
      */
-    private static void getLocationRelativeToParentPagedView(View v, int[] pos) {
-        getPagedViewScrollShift(v, sTempShift);
-        v.getLocationInWindow(sTempPos);
-        pos[0] = sTempPos[0] + sTempShift[0];
-        pos[1] = sTempPos[1] + sTempShift[1];
+    private static void computeLocationRelativeToParent(View v, View parent, int[] pos) {
+        pos[0] = pos[1] = 0;
+        computeLocationRelativeToParentHelper(v, parent, pos);
+
+        // If a view is scaled, its position will also shift accordingly. For optimization, only
+        // consider this for the last node.
+        pos[0] += (1 - v.getScaleX()) * v.getWidth() / 2;
+        pos[1] += (1 - v.getScaleY()) * v.getHeight() / 2;
     }
 
-    private static void getPagedViewScrollShift(View child, int[] shift) {
-        ViewParent parent = child.getParent();
+    private static void computeLocationRelativeToParentHelper(View child,
+            View commonParent, int[] shift) {
+        View parent = (View) child.getParent();
+        shift[0] += child.getLeft();
+        shift[1] += child.getTop();
         if (parent instanceof PagedView) {
-            View parentView = (View) parent;
-            child.getLocationInWindow(sTempPos);
-            shift[0] = parentView.getPaddingLeft() - sTempPos[0];
-            shift[1] = -(int) child.getTranslationY();
-        } else if (parent instanceof View) {
-            getPagedViewScrollShift((View) parent, shift);
-        } else {
-            shift[0] = shift[1] = 0;
+            PagedView page = (PagedView) parent;
+            shift[0] -= page.getScrollForPage(page.indexOfChild(child));
+        }
+
+        if (parent != commonParent) {
+            computeLocationRelativeToParentHelper(parent, commonParent, shift);
         }
     }
 
-    private static final class ViewAnimState {
+    @Thunk static final class ViewAnimState {
         float x, y, scaleX, scaleY;
     }
 }
diff --git a/src/com/android/launcher3/FocusOnlyTabWidget.java b/src/com/android/launcher3/FocusOnlyTabWidget.java
deleted file mode 100644
index 08fc311..0000000
--- a/src/com/android/launcher3/FocusOnlyTabWidget.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2011 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.util.AttributeSet;
-import android.view.View;
-import android.widget.TabWidget;
-
-public class FocusOnlyTabWidget extends TabWidget {
-    public FocusOnlyTabWidget(Context context) {
-        super(context);
-    }
-
-    public FocusOnlyTabWidget(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public FocusOnlyTabWidget(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
-
-    public View getSelectedTab() {
-        final int count = getTabCount();
-        for (int i = 0; i < count; ++i) {
-            View v = getChildTabViewAt(i);
-            if (v.isSelected()) {
-                return v;
-            }
-        }
-        return null;
-    }
-
-    public int getChildTabIndex(View v) {
-        final int tabCount = getTabCount();
-        for (int i = 0; i < tabCount; ++i) {
-            if (getChildTabViewAt(i) == v) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    public void setCurrentTabToFocusedTab() {
-        View tab = null;
-        int index = -1;
-        final int count = getTabCount();
-        for (int i = 0; i < count; ++i) {
-            View v = getChildTabViewAt(i);
-            if (v.hasFocus()) {
-                tab = v;
-                index = i;
-                break;
-            }
-        }
-        if (index > -1) {
-            super.setCurrentTab(index);
-            super.onFocusChange(tab, true);
-        }
-    }
-    public void superOnFocusChange(View v, boolean hasFocus) {
-        super.onFocusChange(v, hasFocus);
-    }
-
-    @Override
-    public void onFocusChange(android.view.View v, boolean hasFocus) {
-        if (v == this && hasFocus && getTabCount() > 0) {
-            getSelectedTab().requestFocus();
-            return;
-        }
-    }
-}
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index 66b6568..c35ce94 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -21,12 +21,13 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.SystemClock;
-import android.support.v4.widget.AutoScrollHelper;
+import android.os.Build;
 import android.text.InputType;
 import android.text.Selection;
 import android.text.Spannable;
@@ -39,124 +40,135 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.AccelerateInterpolator;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.LinearLayout;
-import android.widget.ScrollView;
 import android.widget.TextView;
 
+import com.android.launcher3.DragController.DragListener;
 import com.android.launcher3.FolderInfo.FolderListener;
+import com.android.launcher3.UninstallDropTarget.UninstallSource;
+import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 
 /**
  * Represents a set of icons chosen by the user or generated by the system.
  */
 public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
         View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
-        View.OnFocusChangeListener {
+        View.OnFocusChangeListener, DragListener, UninstallSource {
     private static final String TAG = "Launcher.Folder";
 
-    protected DragController mDragController;
-    protected Launcher mLauncher;
-    protected FolderInfo mInfo;
+    /**
+     * We avoid measuring {@link #mContentWrapper} with a 0 width or height, as this
+     * results in CellLayout being measured as UNSPECIFIED, which it does not support.
+     */
+    private static final int MIN_CONTENT_DIMEN = 5;
 
     static final int STATE_NONE = -1;
     static final int STATE_SMALL = 0;
     static final int STATE_ANIMATING = 1;
     static final int STATE_OPEN = 2;
 
-    private int mExpandDuration;
-    private int mMaterialExpandDuration;
-    private int mMaterialExpandStagger;
-    protected CellLayout mContent;
-    private ScrollView mScrollView;
-    private final LayoutInflater mInflater;
-    private final IconCache mIconCache;
-    private int mState = STATE_NONE;
-    private static final int REORDER_ANIMATION_DURATION = 230;
+    /**
+     * Time for which the scroll hint is shown before automatically changing page.
+     */
+    public static final int SCROLL_HINT_DURATION = DragController.SCROLL_DELAY;
+
+    /**
+     * Time in milliseconds for which an icon sticks to the target position
+     * in case of a sorted folder.
+     */
+    private static final int SORTED_STICKY_REORDER_DELAY = 1500;
+
+    /**
+     * Fraction of icon width which behave as scroll region.
+     */
+    private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
+
     private static final int REORDER_DELAY = 250;
     private static final int ON_EXIT_CLOSE_DELAY = 400;
+    private static final Rect sTempRect = new Rect();
+
+    private static String sDefaultFolderName;
+    private static String sHintText;
+
+    private final Alarm mReorderAlarm = new Alarm();
+    private final Alarm mOnExitAlarm = new Alarm();
+    private final Alarm mOnScrollHintAlarm = new Alarm();
+    @Thunk final Alarm mScrollPauseAlarm = new Alarm();
+
+    @Thunk final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
+
+    private final int mExpandDuration;
+    private final int mMaterialExpandDuration;
+    private final int mMaterialExpandStagger;
+
+    private final InputMethodManager mInputMethodManager;
+
+    protected final Launcher mLauncher;
+    protected DragController mDragController;
+    protected FolderInfo mInfo;
+
+    @Thunk FolderIcon mFolderIcon;
+
+    @Thunk FolderPagedView mContent;
+    @Thunk View mContentWrapper;
+    FolderEditText mFolderName;
+
+    private View mFooter;
+    private int mFooterHeight;
+
+    // Cell ranks used for drag and drop
+    @Thunk int mTargetRank, mPrevTargetRank, mEmptyCellRank;
+
+    @Thunk int mState = STATE_NONE;
     private boolean mRearrangeOnClose = false;
-    private FolderIcon mFolderIcon;
-    private int mMaxCountX;
-    private int mMaxCountY;
-    private int mMaxNumItems;
-    private ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
     boolean mItemsInvalidated = false;
     private ShortcutInfo mCurrentDragInfo;
     private View mCurrentDragView;
     private boolean mIsExternalDrag;
     boolean mSuppressOnAdd = false;
-    private int[] mTargetCell = new int[2];
-    private int[] mPreviousTargetCell = new int[2];
-    private int[] mEmptyCell = new int[2];
-    private Alarm mReorderAlarm = new Alarm();
-    private Alarm mOnExitAlarm = new Alarm();
-    private int mFolderNameHeight;
-    private Rect mTempRect = new Rect();
     private boolean mDragInProgress = false;
     private boolean mDeleteFolderOnDropCompleted = false;
     private boolean mSuppressFolderDeletion = false;
     private boolean mItemAddedBackToSelfViaIcon = false;
-    FolderEditText mFolderName;
-    private float mFolderIconPivotX;
-    private float mFolderIconPivotY;
-
+    @Thunk float mFolderIconPivotX;
+    @Thunk float mFolderIconPivotY;
     private boolean mIsEditingName = false;
-    private InputMethodManager mInputMethodManager;
-
-    private static String sDefaultFolderName;
-    private static String sHintText;
-
-    private FocusIndicatorView mFocusIndicatorHandler;
-
-    // We avoid measuring the scroll view with a 0 width or height, as this
-    // results in CellLayout being measured as UNSPECIFIED, which it does
-    // not support.
-    private static final int MIN_CONTENT_DIMEN = 5;
 
     private boolean mDestroyed;
 
-    private AutoScrollHelper mAutoScrollHelper;
-
-    private Runnable mDeferredAction;
+    @Thunk Runnable mDeferredAction;
     private boolean mDeferDropAfterUninstall;
     private boolean mUninstallSuccessful;
 
+    // Folder scrolling
+    private int mScrollAreaOffset;
+
+    @Thunk int mScrollHintDir = DragController.SCROLL_NONE;
+    @Thunk int mCurrentScrollDir = DragController.SCROLL_NONE;
+
     /**
      * Used to inflate the Workspace from XML.
      *
      * @param context The application's context.
-     * @param attrs The attribtues set containing the Workspace's customization values.
+     * @param attrs The attributes set containing the Workspace's customization values.
      */
     public Folder(Context context, AttributeSet attrs) {
         super(context, attrs);
-
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
         setAlwaysDrawnWithCacheEnabled(false);
-        mInflater = LayoutInflater.from(context);
-        mIconCache = app.getIconCache();
-
-        Resources res = getResources();
-        mMaxCountX = (int) grid.numColumns;
-        // Allow scrolling folders when DISABLE_ALL_APPS is true.
-        if (LauncherAppState.isDisableAllApps()) {
-            mMaxCountY = mMaxNumItems = Integer.MAX_VALUE;
-        } else {
-            mMaxCountY = (int) grid.numRows;
-            mMaxNumItems = mMaxCountX * mMaxCountY;
-        }
-
         mInputMethodManager = (InputMethodManager)
                 getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
 
+        Resources res = getResources();
         mExpandDuration = res.getInteger(R.integer.config_folderExpandDuration);
         mMaterialExpandDuration = res.getInteger(R.integer.config_materialFolderExpandDuration);
         mMaterialExpandStagger = res.getInteger(R.integer.config_materialFolderExpandStagger);
@@ -170,45 +182,38 @@
         mLauncher = (Launcher) context;
         // We need this view to be focusable in touch mode so that when text editing of the folder
         // name is complete, we have something to focus on, thus hiding the cursor and giving
-        // reliable behvior when clicking the text field (since it will always gain focus on click).
+        // reliable behavior when clicking the text field (since it will always gain focus on click).
         setFocusableInTouchMode(true);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mScrollView = (ScrollView) findViewById(R.id.scroll_view);
-        mContent = (CellLayout) findViewById(R.id.folder_content);
+        mContentWrapper = findViewById(R.id.folder_content_wrapper);
+        mContent = (FolderPagedView) findViewById(R.id.folder_content);
+        mContent.setFolder(this);
 
-        mFocusIndicatorHandler = new FocusIndicatorView(getContext());
-        mContent.addView(mFocusIndicatorHandler, 0);
-        mFocusIndicatorHandler.getLayoutParams().height = FocusIndicatorView.DEFAULT_LAYOUT_SIZE;
-        mFocusIndicatorHandler.getLayoutParams().width = FocusIndicatorView.DEFAULT_LAYOUT_SIZE;
-
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-
-        mContent.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
-        mContent.setGridSize(0, 0);
-        mContent.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
-        mContent.setInvertIfRtl(true);
         mFolderName = (FolderEditText) findViewById(R.id.folder_name);
         mFolderName.setFolder(this);
         mFolderName.setOnFocusChangeListener(this);
 
-        // We find out how tall the text view wants to be (it is set to wrap_content), so that
-        // we can allocate the appropriate amount of space for it.
-        int measureSpec = MeasureSpec.UNSPECIFIED;
-        mFolderName.measure(measureSpec, measureSpec);
-        mFolderNameHeight = mFolderName.getMeasuredHeight();
-
         // We disable action mode for now since it messes up the view on phones
         mFolderName.setCustomSelectionActionModeCallback(mActionModeCallback);
         mFolderName.setOnEditorActionListener(this);
         mFolderName.setSelectAllOnFocus(true);
         mFolderName.setInputType(mFolderName.getInputType() |
                 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
-        mAutoScrollHelper = new FolderAutoScrollHelper(mScrollView);
+
+        mFooter = findViewById(R.id.folder_footer);
+        updateFooterHeight();
+    }
+
+    public void updateFooterHeight() {
+        // We find out how tall footer wants to be (it is set to wrap_content), so that
+        // we can allocate the appropriate amount of space for it.
+        int measureSpec = MeasureSpec.UNSPECIFIED;
+        mFooter.measure(measureSpec, measureSpec);
+        mFooterHeight = mFooter.getMeasuredHeight();
     }
 
     private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
@@ -246,14 +251,13 @@
                 return false;
             }
 
-            mLauncher.getWorkspace().beginDragShared(v, this);
+            mLauncher.getWorkspace().beginDragShared(v, new Point(), this, false);
 
             mCurrentDragInfo = item;
-            mEmptyCell[0] = item.cellX;
-            mEmptyCell[1] = item.cellY;
+            mEmptyCellRank = item.rank;
             mCurrentDragView = v;
 
-            mContent.removeView(mCurrentDragView);
+            mContent.removeItem(mCurrentDragView);
             mInfo.remove(mCurrentDragInfo);
             mDragInProgress = true;
             mItemAddedBackToSelfViaIcon = false;
@@ -307,10 +311,6 @@
         return mFolderName;
     }
 
-    public CellLayout getContent() {
-        return mContent;
-    }
-
     /**
      * We need to handle touch events to prevent them from falling through to the workspace below.
      */
@@ -323,7 +323,7 @@
         mDragController = dragController;
     }
 
-    void setFolderIcon(FolderIcon icon) {
+    public void setFolderIcon(FolderIcon icon) {
         mFolderIcon = icon;
     }
 
@@ -343,30 +343,9 @@
     void bind(FolderInfo info) {
         mInfo = info;
         ArrayList<ShortcutInfo> children = info.contents;
-        ArrayList<ShortcutInfo> overflow = new ArrayList<ShortcutInfo>();
-
-        final int totalChildren = children.size();
-        setupContentForNumItems(totalChildren);
-
-        // Arrange children in the grid based on the rank.
         Collections.sort(children, Utilities.RANK_COMPARATOR);
-        final int countX = mContent.getCountX();
 
-        int visibleChildren = 0;
-        for (int i = 0; i < children.size(); i++) {
-            ShortcutInfo child = children.get(i);
-            child.cellX = i % countX;
-            child.cellY = i / countX;
-
-            if (createAndAddShortcut(child) == null) {
-                overflow.add(child);
-            } else {
-                visibleChildren++;
-            }
-        }
-
-        // We rearrange the items in case there are any empty gaps
-        setupContentForNumItems(visibleChildren);
+        ArrayList<ShortcutInfo> overflow = mContent.bindItems(children);
 
         // If our folder has too many items we prune them from the list. This is an issue
         // when upgrading from the old Folders implementation which could contain an unlimited
@@ -376,6 +355,14 @@
             LauncherModel.deleteItemFromDatabase(mLauncher, item);
         }
 
+        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+        if (lp == null) {
+            lp = new DragLayer.LayoutParams(0, 0);
+            lp.customPosition = true;
+            setLayoutParams(lp);
+        }
+        centerAboutIcon();
+
         mItemsInvalidated = true;
         updateTextViewFocus();
         mInfo.addListener(this);
@@ -429,6 +416,12 @@
     public void animateOpen() {
         if (!(getParent() instanceof DragLayer)) return;
 
+        mContent.completePendingPageChanges();
+        if (!(mDragInProgress && mContent.mIsSorted)) {
+            // Open on the first page.
+            mContent.snapToPageImmediately(0);
+        }
+
         Animator openFolderAnim = null;
         final Runnable onCompleteRunnable;
         if (!Utilities.isLmpOrAbove()) {
@@ -473,14 +466,14 @@
             reveal.setDuration(mMaterialExpandDuration);
             reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
 
-            mContent.setAlpha(0f);
-            Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContent, "alpha", 0f, 1f);
+            mContentWrapper.setAlpha(0f);
+            Animator iconsAlpha = LauncherAnimUtils.ofFloat(mContentWrapper, "alpha", 0f, 1f);
             iconsAlpha.setDuration(mMaterialExpandDuration);
             iconsAlpha.setStartDelay(mMaterialExpandStagger);
             iconsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
 
-            mFolderName.setAlpha(0f);
-            Animator textAlpha = LauncherAnimUtils.ofFloat(mFolderName, "alpha", 0f, 1f);
+            mFooter.setAlpha(0f);
+            Animator textAlpha = LauncherAnimUtils.ofFloat(mFooter, "alpha", 0f, 1f);
             textAlpha.setDuration(mMaterialExpandDuration);
             textAlpha.setStartDelay(mMaterialExpandStagger);
             textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
@@ -497,11 +490,11 @@
 
             openFolderAnim = anim;
 
-            mContent.setLayerType(LAYER_TYPE_HARDWARE, null);
+            mContentWrapper.setLayerType(LAYER_TYPE_HARDWARE, null);
             onCompleteRunnable = new Runnable() {
                 @Override
                 public void run() {
-                    mContent.setLayerType(LAYER_TYPE_NONE, null);
+                    mContentWrapper.setLayerType(LAYER_TYPE_NONE, null);
                 }
             };
         }
@@ -509,8 +502,7 @@
             @Override
             public void onAnimationStart(Animator animation) {
                 sendCustomAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                        String.format(getContext().getString(R.string.folder_opened),
-                        mContent.getCountX(), mContent.getCountY()));
+                        mContent.getAccessibilityDescription());
                 mState = STATE_ANIMATING;
             }
             @Override
@@ -521,7 +513,7 @@
                     onCompleteRunnable.run();
                 }
 
-                setFocusOnFirstChild();
+                mContent.setFocusOnFirstChild();
             }
         });
         openFolderAnim.start();
@@ -530,21 +522,40 @@
         if (mDragController.isDragging()) {
             mDragController.forceTouchMove();
         }
+
+        FolderPagedView pages = (FolderPagedView) mContent;
+        pages.verifyVisibleHighResIcons(pages.getNextPage());
     }
 
     public void beginExternalDrag(ShortcutInfo item) {
-        setupContentForNumItems(getItemCount() + 1);
-        findAndSetEmptyCells(item);
-
         mCurrentDragInfo = item;
-        mEmptyCell[0] = item.cellX;
-        mEmptyCell[1] = item.cellY;
+        mEmptyCellRank = mContent.allocateRankForNewItem(item);
         mIsExternalDrag = true;
-
         mDragInProgress = true;
+
+        if (mContent.mIsSorted) {
+            mScrollPauseAlarm.setOnAlarmListener(null);
+            mScrollPauseAlarm.cancelAlarm();
+            mScrollPauseAlarm.setAlarm(SORTED_STICKY_REORDER_DELAY);
+        }
+
+        // Since this folder opened by another controller, it might not get onDrop or
+        // onDropComplete. Perform cleanup once drag-n-drop ends.
+        mDragController.addDragListener(this);
     }
 
-    private void sendCustomAccessibilityEvent(int type, String text) {
+    @Override
+    public void onDragStart(DragSource source, Object info, int dragAction) { }
+
+    @Override
+    public void onDragEnd() {
+        if (mIsExternalDrag && mDragInProgress) {
+            completeDragExit();
+        }
+        mDragController.removeDragListener(this);
+    }
+
+    @Thunk void sendCustomAccessibilityEvent(int type, String text) {
         AccessibilityManager accessibilityManager = (AccessibilityManager)
                 getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
         if (accessibilityManager.isEnabled()) {
@@ -555,13 +566,6 @@
         }
     }
 
-    private void setFocusOnFirstChild() {
-        View firstChild = mContent.getChildAt(0, 0);
-        if (firstChild != null) {
-            firstChild.requestFocus();
-        }
-    }
-
     public void animateClosed() {
         if (!(getParent() instanceof DragLayer)) return;
         PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0);
@@ -597,174 +601,90 @@
                     !isFull());
     }
 
-    protected boolean findAndSetEmptyCells(ShortcutInfo item) {
-        int[] emptyCell = new int[2];
-        if (mContent.findCellForSpan(emptyCell, item.spanX, item.spanY)) {
-            item.cellX = emptyCell[0];
-            item.cellY = emptyCell[1];
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    protected View createAndAddShortcut(ShortcutInfo item) {
-        final BubbleTextView textView =
-            (BubbleTextView) mInflater.inflate(R.layout.folder_application, this, false);
-        textView.applyFromShortcutInfo(item, mIconCache, false);
-
-        textView.setOnClickListener(this);
-        textView.setOnLongClickListener(this);
-        textView.setOnFocusChangeListener(mFocusIndicatorHandler);
-
-        // We need to check here to verify that the given item's location isn't already occupied
-        // by another item.
-        if (mContent.getChildAt(item.cellX, item.cellY) != null || item.cellX < 0 || item.cellY < 0
-                || item.cellX >= mContent.getCountX() || item.cellY >= mContent.getCountY()) {
-            // This shouldn't happen, log it.
-            Log.e(TAG, "Folder order not properly persisted during bind");
-            if (!findAndSetEmptyCells(item)) {
-                return null;
-            }
-        }
-
-        CellLayout.LayoutParams lp =
-            new CellLayout.LayoutParams(item.cellX, item.cellY, item.spanX, item.spanY);
-        boolean insert = false;
-        textView.setOnKeyListener(new FolderKeyEventListener());
-        mContent.addViewToCellLayout(textView, insert ? 0 : -1, (int)item.id, lp, true);
-        return textView;
-    }
-
     public void onDragEnter(DragObject d) {
-        mPreviousTargetCell[0] = -1;
-        mPreviousTargetCell[1] = -1;
+        mPrevTargetRank = -1;
         mOnExitAlarm.cancelAlarm();
+        // Get the area offset such that the folder only closes if half the drag icon width
+        // is outside the folder area
+        mScrollAreaOffset = d.dragView.getDragRegionWidth() / 2 - d.xOffset;
     }
 
     OnAlarmListener mReorderAlarmListener = new OnAlarmListener() {
         public void onAlarm(Alarm alarm) {
-            realTimeReorder(mEmptyCell, mTargetCell);
+            mContent.realTimeReorder(mEmptyCellRank, mTargetRank);
+            mEmptyCellRank = mTargetRank;
         }
     };
 
-    boolean readingOrderGreaterThan(int[] v1, int[] v2) {
-        if (v1[1] > v2[1] || (v1[1] == v2[1] && v1[0] > v2[0])) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    private void realTimeReorder(int[] empty, int[] target) {
-        boolean wrap;
-        int startX;
-        int endX;
-        int startY;
-        int delay = 0;
-        float delayAmount = 30;
-        if (readingOrderGreaterThan(target, empty)) {
-            wrap = empty[0] >= mContent.getCountX() - 1;
-            startY = wrap ? empty[1] + 1 : empty[1];
-            for (int y = startY; y <= target[1]; y++) {
-                startX = y == empty[1] ? empty[0] + 1 : 0;
-                endX = y < target[1] ? mContent.getCountX() - 1 : target[0];
-                for (int x = startX; x <= endX; x++) {
-                    View v = mContent.getChildAt(x,y);
-                    if (mContent.animateChildToPosition(v, empty[0], empty[1],
-                            REORDER_ANIMATION_DURATION, delay, true, true)) {
-                        empty[0] = x;
-                        empty[1] = y;
-                        delay += delayAmount;
-                        delayAmount *= 0.9;
-                    }
-                }
-            }
-        } else {
-            wrap = empty[0] == 0;
-            startY = wrap ? empty[1] - 1 : empty[1];
-            for (int y = startY; y >= target[1]; y--) {
-                startX = y == empty[1] ? empty[0] - 1 : mContent.getCountX() - 1;
-                endX = y > target[1] ? 0 : target[0];
-                for (int x = startX; x >= endX; x--) {
-                    View v = mContent.getChildAt(x,y);
-                    if (mContent.animateChildToPosition(v, empty[0], empty[1],
-                            REORDER_ANIMATION_DURATION, delay, true, true)) {
-                        empty[0] = x;
-                        empty[1] = y;
-                        delay += delayAmount;
-                        delayAmount *= 0.9;
-                    }
-                }
-            }
-        }
-    }
-
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
     public boolean isLayoutRtl() {
         return (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
     }
 
+    @Override
     public void onDragOver(DragObject d) {
-        final DragView dragView = d.dragView;
-        final int scrollOffset = mScrollView.getScrollY();
-        final float[] r = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, dragView, null);
-        r[0] -= getPaddingLeft();
-        r[1] -= getPaddingTop();
+        onDragOver(d, REORDER_DELAY);
+    }
 
-        final long downTime = SystemClock.uptimeMillis();
-        final MotionEvent translatedEv = MotionEvent.obtain(
-                downTime, downTime, MotionEvent.ACTION_MOVE, d.x, d.y, 0);
+    private int getTargetRank(DragObject d, float[] recycle) {
+        recycle = d.getVisualCenter(recycle);
+        return mContent.findNearestArea(
+                (int) recycle[0] - getPaddingLeft(), (int) recycle[1] - getPaddingTop());
+    }
 
-        if (!mAutoScrollHelper.isEnabled()) {
-            mAutoScrollHelper.setEnabled(true);
+    @Thunk void onDragOver(DragObject d, int reorderDelay) {
+        if (mScrollPauseAlarm.alarmPending()) {
+            return;
+        }
+        final float[] r = new float[2];
+        mTargetRank = getTargetRank(d, r);
+
+        if (mTargetRank != mPrevTargetRank) {
+            mReorderAlarm.cancelAlarm();
+            mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
+            mReorderAlarm.setAlarm(REORDER_DELAY);
+            mPrevTargetRank = mTargetRank;
         }
 
-        final boolean handled = mAutoScrollHelper.onTouch(this, translatedEv);
-        translatedEv.recycle();
+        float x = r[0];
+        int currentPage = mContent.getNextPage();
 
-        if (handled) {
-            mReorderAlarm.cancelAlarm();
+        float cellOverlap = mContent.getCurrentCellLayout().getCellWidth()
+                * ICON_OVERSCROLL_WIDTH_FACTOR;
+        boolean isOutsideLeftEdge = x < cellOverlap;
+        boolean isOutsideRightEdge = x > (getWidth() - cellOverlap);
+
+        if (currentPage > 0 && (mContent.rtlLayout ? isOutsideRightEdge : isOutsideLeftEdge)) {
+            showScrollHint(DragController.SCROLL_LEFT, d);
+        } else if (currentPage < (mContent.getPageCount() - 1)
+                && (mContent.rtlLayout ? isOutsideLeftEdge : isOutsideRightEdge)) {
+            showScrollHint(DragController.SCROLL_RIGHT, d);
         } else {
-            mTargetCell = mContent.findNearestArea(
-                    (int) r[0], (int) r[1] + scrollOffset, 1, 1, mTargetCell);
-            if (isLayoutRtl()) {
-                mTargetCell[0] = mContent.getCountX() - mTargetCell[0] - 1;
-            }
-            if (mTargetCell[0] != mPreviousTargetCell[0]
-                    || mTargetCell[1] != mPreviousTargetCell[1]) {
-                mReorderAlarm.cancelAlarm();
-                mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
-                mReorderAlarm.setAlarm(REORDER_DELAY);
-                mPreviousTargetCell[0] = mTargetCell[0];
-                mPreviousTargetCell[1] = mTargetCell[1];
+            mOnScrollHintAlarm.cancelAlarm();
+            if (mScrollHintDir != DragController.SCROLL_NONE) {
+                mContent.clearScrollHint();
+                mScrollHintDir = DragController.SCROLL_NONE;
             }
         }
     }
 
-    // This is used to compute the visual center of the dragView. The idea is that
-    // the visual center represents the user's interpretation of where the item is, and hence
-    // is the appropriate point to use when determining drop location.
-    private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset,
-            DragView dragView, float[] recycle) {
-        float res[];
-        if (recycle == null) {
-            res = new float[2];
-        } else {
-            res = recycle;
+    private void showScrollHint(int direction, DragObject d) {
+        // Show scroll hint on the right
+        if (mScrollHintDir != direction) {
+            mContent.showScrollHint(direction);
+            mScrollHintDir = direction;
         }
 
-        // These represent the visual top and left of drag view if a dragRect was provided.
-        // If a dragRect was not provided, then they correspond to the actual view left and
-        // top, as the dragRect is in that case taken to be the entire dragView.
-        // R.dimen.dragViewOffsetY.
-        int left = x - xOffset;
-        int top = y - yOffset;
+        // Set alarm for when the hint is complete
+        if (!mOnScrollHintAlarm.alarmPending() || mCurrentScrollDir != direction) {
+            mCurrentScrollDir = direction;
+            mOnScrollHintAlarm.cancelAlarm();
+            mOnScrollHintAlarm.setOnAlarmListener(new OnScrollHintListener(d));
+            mOnScrollHintAlarm.setAlarm(SCROLL_HINT_DURATION);
 
-        // In order to find the visual center, we shift by half the dragRect
-        res[0] = left + dragView.getDragRegion().width() / 2;
-        res[1] = top + dragView.getDragRegion().height() / 2;
-
-        return res;
+            mReorderAlarm.cancelAlarm();
+            mTargetRank = mEmptyCellRank;
+        }
     }
 
     OnAlarmListener mOnExitAlarmListener = new OnAlarmListener() {
@@ -774,17 +694,19 @@
     };
 
     public void completeDragExit() {
-        mLauncher.closeFolder();
+        if (mInfo.opened) {
+            mLauncher.closeFolder();
+            mRearrangeOnClose = true;
+        } else {
+            rearrangeChildren();
+        }
         mCurrentDragInfo = null;
         mCurrentDragView = null;
         mSuppressOnAdd = false;
-        mRearrangeOnClose = true;
         mIsExternalDrag = false;
     }
 
     public void onDragExit(DragObject d) {
-        // Exiting folder; stop the auto scroller.
-        mAutoScrollHelper.setEnabled(false);
         // We only close the folder if this is a true drag exit, ie. not because
         // a drop has occurred above the folder.
         if (!d.dragComplete) {
@@ -792,6 +714,13 @@
             mOnExitAlarm.setAlarm(ON_EXIT_CLOSE_DELAY);
         }
         mReorderAlarm.cancelAlarm();
+
+        mOnScrollHintAlarm.cancelAlarm();
+        mScrollPauseAlarm.cancelAlarm();
+        if (mScrollHintDir != DragController.SCROLL_NONE) {
+            mContent.clearScrollHint();
+            mScrollHintDir = DragController.SCROLL_NONE;
+        }
     }
 
     public void onDropCompleted(final View target, final DragObject d,
@@ -816,7 +745,7 @@
                 replaceFolderWithFinalItem();
             }
         } else {
-            setupContentForNumItems(getItemCount());
+            rearrangeChildren();
             // The drag failed, we need to return the item to the folder
             mFolderIcon.onDrop(d);
         }
@@ -827,6 +756,7 @@
                 if (!successfulDrop) {
                     mSuppressFolderDeletion = true;
                 }
+                mScrollPauseAlarm.cancelAlarm();
                 completeDragExit();
             }
         }
@@ -843,10 +773,12 @@
         updateItemLocationsInDatabaseBatch();
     }
 
+    @Override
     public void deferCompleteDropAfterUninstallActivity() {
         mDeferDropAfterUninstall = true;
     }
 
+    @Override
     public void onUninstallActivityReturned(boolean success) {
         mDeferDropAfterUninstall = false;
         mUninstallSuccessful = success;
@@ -903,7 +835,7 @@
             View v = list.get(i);
             ItemInfo info = (ItemInfo) v.getTag();
             LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0,
-                        info.cellX, info.cellY, false);
+                        info.cellX, info.cellY);
         }
     }
 
@@ -917,37 +849,8 @@
         return true;
     }
 
-    private void setupContentDimensions(int count) {
-        ArrayList<View> list = getItemsInReadingOrder();
-
-        int countX = mContent.getCountX();
-        int countY = mContent.getCountY();
-        boolean done = false;
-
-        while (!done) {
-            int oldCountX = countX;
-            int oldCountY = countY;
-            if (countX * countY < count) {
-                // Current grid is too small, expand it
-                if ((countX <= countY || countY == mMaxCountY) && countX < mMaxCountX) {
-                    countX++;
-                } else if (countY < mMaxCountY) {
-                    countY++;
-                }
-                if (countY == 0) countY++;
-            } else if ((countY - 1) * countX >= count && countY >= countX) {
-                countY = Math.max(0, countY - 1);
-            } else if ((countX - 1) * countY >= count) {
-                countX = Math.max(0, countX - 1);
-            }
-            done = countX == oldCountX && countY == oldCountY;
-        }
-        mContent.setGridSize(countX, countY);
-        arrangeChildren(list);
-    }
-
     public boolean isFull() {
-        return getItemCount() >= mMaxNumItems;
+        return mContent.isFull();
     }
 
     private void centerAboutIcon() {
@@ -957,13 +860,13 @@
         int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
         int height = getFolderHeight();
 
-        float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, mTempRect);
+        float scale = parent.getDescendantRectRelativeToSelf(mFolderIcon, sTempRect);
 
         LauncherAppState app = LauncherAppState.getInstance();
         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
 
-        int centerX = (int) (mTempRect.left + mTempRect.width() * scale / 2);
-        int centerY = (int) (mTempRect.top + mTempRect.height() * scale / 2);
+        int centerX = (int) (sTempRect.left + sTempRect.width() * scale / 2);
+        int centerY = (int) (sTempRect.top + sTempRect.height() * scale / 2);
         int centeredLeft = centerX - width / 2;
         int centeredTop = centerY - height / 2;
         int currentPage = mLauncher.getWorkspace().getNextPage();
@@ -1016,18 +919,6 @@
         return mFolderIconPivotY;
     }
 
-    private void setupContentForNumItems(int count) {
-        setupContentDimensions(count);
-
-        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
-        if (lp == null) {
-            lp = new DragLayer.LayoutParams(0, 0);
-            lp.customPosition = true;
-            setLayoutParams(lp);
-        }
-        centerAboutIcon();
-    }
-
     private int getContentAreaHeight() {
         LauncherAppState app = LauncherAppState.getInstance();
         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
@@ -1035,7 +926,7 @@
                 CellLayout.LANDSCAPE : CellLayout.PORTRAIT);
         int maxContentAreaHeight = grid.availableHeightPx -
                 workspacePadding.top - workspacePadding.bottom -
-                mFolderNameHeight;
+                mFooterHeight;
         int height = Math.min(maxContentAreaHeight,
                 mContent.getDesiredHeight());
         return Math.max(height, MIN_CONTENT_DIMEN);
@@ -1046,67 +937,58 @@
     }
 
     private int getFolderHeight() {
-        int height = getPaddingTop() + getPaddingBottom()
-                + getContentAreaHeight() + mFolderNameHeight;
-        return height;
+        return getFolderHeight(getContentAreaHeight());
+    }
+
+    private int getFolderHeight(int contentAreaHeight) {
+        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
     }
 
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int width = getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
-        int height = getFolderHeight();
-        int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(getContentAreaWidth(),
-                MeasureSpec.EXACTLY);
-        int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(getContentAreaHeight(),
-                MeasureSpec.EXACTLY);
+        int contentWidth = getContentAreaWidth();
+        int contentHeight = getContentAreaHeight();
 
-        if (LauncherAppState.isDisableAllApps()) {
-            // Don't cap the height of the content to allow scrolling.
-            mContent.setFixedSize(getContentAreaWidth(), mContent.getDesiredHeight());
-        } else {
-            mContent.setFixedSize(getContentAreaWidth(), getContentAreaHeight());
-        }
+        int contentAreaWidthSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY);
+        int contentAreaHeightSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY);
 
-        mScrollView.measure(contentAreaWidthSpec, contentAreaHeightSpec);
-        mFolderName.measure(contentAreaWidthSpec,
-                MeasureSpec.makeMeasureSpec(mFolderNameHeight, MeasureSpec.EXACTLY));
-        setMeasuredDimension(width, height);
+        mContent.setFixedSize(contentWidth, contentHeight);
+        mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
+        mFooter.measure(contentAreaWidthSpec,
+                MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
+
+        int folderWidth = getPaddingLeft() + getPaddingRight() + contentWidth;
+        int folderHeight = getFolderHeight(contentHeight);
+        setMeasuredDimension(folderWidth, folderHeight);
     }
 
-    private void arrangeChildren(ArrayList<View> list) {
-        int[] vacant = new int[2];
-        if (list == null) {
-            list = getItemsInReadingOrder();
-        }
-        mContent.removeAllViews();
+    /**
+     * Rearranges the children based on their rank.
+     */
+    public void rearrangeChildren() {
+        rearrangeChildren(-1);
+    }
 
-        for (int i = 0; i < list.size(); i++) {
-            View v = list.get(i);
-            mContent.getVacantCell(vacant, 1, 1);
-            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
-            lp.cellX = vacant[0];
-            lp.cellY = vacant[1];
-            ItemInfo info = (ItemInfo) v.getTag();
-            if (info.cellX != vacant[0] || info.cellY != vacant[1]) {
-                info.cellX = vacant[0];
-                info.cellY = vacant[1];
-                LauncherModel.addOrMoveItemInDatabase(mLauncher, info, mInfo.id, 0,
-                        info.cellX, info.cellY);
-            }
-            boolean insert = false;
-            mContent.addViewToCellLayout(v, insert ? 0 : -1, (int)info.id, lp, true);
-        }
+    /**
+     * Rearranges the children based on their rank.
+     * @param itemCount if greater than the total children count, empty spaces are left at the end,
+     * otherwise it is ignored.
+     */
+    public void rearrangeChildren(int itemCount) {
+        ArrayList<View> views = getItemsInReadingOrder();
+        mContent.arrangeChildren(views, Math.max(itemCount, views.size()));
         mItemsInvalidated = true;
     }
 
+    // TODO remove this once GSA code fix is submitted
+    public ViewGroup getContent() {
+        return (ViewGroup) mContent;
+    }
+
     public int getItemCount() {
-        return mContent.getShortcutsAndWidgets().getChildCount();
+        return mContent.getItemCount();
     }
 
-    public View getItemAt(int index) {
-        return mContent.getShortcutsAndWidgets().getChildAt(index);
-    }
-
-    private void onCloseComplete() {
+    @Thunk void onCloseComplete() {
         DragLayer parent = (DragLayer) getParent();
         if (parent != null) {
             parent.removeView(this);
@@ -1116,7 +998,7 @@
         mFolderIcon.requestFocus();
 
         if (mRearrangeOnClose) {
-            setupContentForNumItems(getItemCount());
+            rearrangeChildren();
             mRearrangeOnClose = false;
         }
         if (getItemCount() <= 1) {
@@ -1129,7 +1011,7 @@
         mSuppressFolderDeletion = false;
     }
 
-    private void replaceFolderWithFinalItem() {
+    @Thunk void replaceFolderWithFinalItem() {
         // Add the last remaining child to the workspace in place of the folder
         Runnable onCompleteRunnable = new Runnable() {
             @Override
@@ -1166,7 +1048,7 @@
                 }
             }
         };
-        View finalChild = getItemAt(0);
+        View finalChild = mContent.getLastItem();
         if (finalChild != null) {
             mFolderIcon.performDestroyAnimation(finalChild, onCompleteRunnable);
         } else {
@@ -1181,9 +1063,8 @@
 
     // This method keeps track of the last item in the folder for the purposes
     // of keyboard focus
-    private void updateTextViewFocus() {
-        View lastChild = getItemAt(getItemCount() - 1);
-        getItemAt(getItemCount() - 1);
+    public void updateTextViewFocus() {
+        View lastChild = mContent.getLastItem();
         if (lastChild != null) {
             mFolderName.setNextFocusDownId(lastChild.getId());
             mFolderName.setNextFocusRightId(lastChild.getId());
@@ -1208,12 +1089,24 @@
             };
         }
 
+        // If the icon was dropped while the page was being scrolled, we need to compute
+        // the target location again such that the icon is placed of the final page.
+        if (!mContent.rankOnCurrentPage(mEmptyCellRank)) {
+            // Reorder again.
+            mTargetRank = getTargetRank(d, null);
+
+            // Rearrange items immediately.
+            mReorderAlarmListener.onAlarm(mReorderAlarm);
+
+            mOnScrollHintAlarm.cancelAlarm();
+            mScrollPauseAlarm.cancelAlarm();
+        }
+        mContent.completePendingPageChanges();
+
         View currentDragView;
         ShortcutInfo si = mCurrentDragInfo;
         if (mIsExternalDrag) {
-            si.cellX = mEmptyCell[0];
-            si.cellY = mEmptyCell[1];
-
+            currentDragView = mContent.createAndAddViewForRank(si, mEmptyCellRank);
             // Actually move the item in the database if it was an external drag. Call this
             // before creating the view, so that ShortcutInfo is updated appropriately.
             LauncherModel.addOrMoveItemInDatabase(
@@ -1224,14 +1117,9 @@
                 updateItemLocationsInDatabaseBatch();
             }
             mIsExternalDrag = false;
-
-            currentDragView = createAndAddShortcut(si);
         } else {
             currentDragView = mCurrentDragView;
-            CellLayout.LayoutParams lp = (CellLayout.LayoutParams) currentDragView.getLayoutParams();
-            si.cellX = lp.cellX = mEmptyCell[0];
-            si.cellX = lp.cellY = mEmptyCell[1];
-            mContent.addViewToCellLayout(currentDragView, -1, (int) si.id, lp, true);
+            mContent.addViewForRank(currentDragView, si, mEmptyCellRank);
         }
 
         if (d.dragView.hasDrawn()) {
@@ -1250,7 +1138,7 @@
             currentDragView.setVisibility(VISIBLE);
         }
         mItemsInvalidated = true;
-        setupContentDimensions(getItemCount());
+        rearrangeChildren();
 
         // Temporarily suppress the listener, as we did all the work already here.
         mSuppressOnAdd = true;
@@ -1258,6 +1146,7 @@
         mSuppressOnAdd = false;
         // Clear the drag info, as it is no longer being dragged.
         mCurrentDragInfo = null;
+        mDragInProgress = false;
     }
 
     // This is used so the item doesn't immediately appear in the folder when added. In one case
@@ -1273,16 +1162,11 @@
     }
 
     public void onAdd(ShortcutInfo item) {
-        mItemsInvalidated = true;
         // If the item was dropped onto this open folder, we have done the work associated
         // with adding the item to the folder, as indicated by mSuppressOnAdd being set
         if (mSuppressOnAdd) return;
-        if (!findAndSetEmptyCells(item)) {
-            // The current layout is full, can we expand it?
-            setupContentForNumItems(getItemCount() + 1);
-            findAndSetEmptyCells(item);
-        }
-        createAndAddShortcut(item);
+        mContent.createAndAddViewForRank(item, mContent.allocateRankForNewItem(item));
+        mItemsInvalidated = true;
         LauncherModel.addOrMoveItemInDatabase(
                 mLauncher, item, mInfo.id, 0, item.cellX, item.cellY);
     }
@@ -1293,27 +1177,25 @@
         // the work associated with removing the item, so we don't have to do anything here.
         if (item == mCurrentDragInfo) return;
         View v = getViewForInfo(item);
-        mContent.removeView(v);
+        mContent.removeItem(v);
         if (mState == STATE_ANIMATING) {
             mRearrangeOnClose = true;
         } else {
-            setupContentForNumItems(getItemCount());
+            rearrangeChildren();
         }
         if (getItemCount() <= 1) {
             replaceFolderWithFinalItem();
         }
     }
 
-    private View getViewForInfo(ShortcutInfo item) {
-        for (int j = 0; j < mContent.getCountY(); j++) {
-            for (int i = 0; i < mContent.getCountX(); i++) {
-                View v = mContent.getChildAt(i, j);
-                if (v.getTag() == item) {
-                    return v;
-                }
+    private View getViewForInfo(final ShortcutInfo item) {
+        return mContent.iterateOverItems(new ItemOperator() {
+
+            @Override
+            public boolean evaluate(ItemInfo info, View view, View parent) {
+                return info == item;
             }
-        }
-        return null;
+        });
     }
 
     public void onItemsChanged() {
@@ -1326,14 +1208,14 @@
     public ArrayList<View> getItemsInReadingOrder() {
         if (mItemsInvalidated) {
             mItemsInReadingOrder.clear();
-            for (int j = 0; j < mContent.getCountY(); j++) {
-                for (int i = 0; i < mContent.getCountX(); i++) {
-                    View v = mContent.getChildAt(i, j);
-                    if (v != null) {
-                        mItemsInReadingOrder.add(v);
-                    }
+            mContent.iterateOverItems(new ItemOperator() {
+
+                @Override
+                public boolean evaluate(ItemInfo info, View view, View parent) {
+                    mItemsInReadingOrder.add(view);
+                    return false;
                 }
-            }
+            });
             mItemsInvalidated = false;
         }
         return mItemsInReadingOrder;
@@ -1352,5 +1234,56 @@
     @Override
     public void getHitRectRelativeToDragLayer(Rect outRect) {
         getHitRect(outRect);
+        outRect.left -= mScrollAreaOffset;
+        outRect.right += mScrollAreaOffset;
+    }
+
+    private class OnScrollHintListener implements OnAlarmListener {
+
+        private final DragObject mDragObject;
+
+        OnScrollHintListener(DragObject object) {
+            mDragObject = object;
+        }
+
+        /**
+         * Scroll hint has been shown long enough. Now scroll to appropriate page.
+         */
+        @Override
+        public void onAlarm(Alarm alarm) {
+            if (mCurrentScrollDir == DragController.SCROLL_LEFT) {
+                mContent.scrollLeft();
+                mScrollHintDir = DragController.SCROLL_NONE;
+            } else if (mCurrentScrollDir == DragController.SCROLL_RIGHT) {
+                mContent.scrollRight();
+                mScrollHintDir = DragController.SCROLL_NONE;
+            } else {
+                // This should not happen
+                return;
+            }
+            mCurrentScrollDir = DragController.SCROLL_NONE;
+
+            // Pause drag event until the scrolling is finished
+            mScrollPauseAlarm.setOnAlarmListener(new OnScrollFinishedListener(mDragObject));
+            mScrollPauseAlarm.setAlarm(DragController.RESCROLL_DELAY);
+        }
+    }
+
+    private class OnScrollFinishedListener implements OnAlarmListener {
+
+        private final DragObject mDragObject;
+
+        OnScrollFinishedListener(DragObject object) {
+            mDragObject = object;
+        }
+
+        /**
+         * Page scroll is complete.
+         */
+        @Override
+        public void onAlarm(Alarm alarm) {
+            // Reorder immediately on page change.
+            onDragOver(mDragObject, 1);
+        }
     }
 }
diff --git a/src/com/android/launcher3/FolderAutoScrollHelper.java b/src/com/android/launcher3/FolderAutoScrollHelper.java
deleted file mode 100644
index 40e8884..0000000
--- a/src/com/android/launcher3/FolderAutoScrollHelper.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.support.v4.widget.AutoScrollHelper;
-import android.widget.ScrollView;
-
-/**
- * An implementation of {@link AutoScrollHelper} that knows how to scroll
- * through a {@link Folder}.
- */
-public class FolderAutoScrollHelper extends AutoScrollHelper {
-    private static final float MAX_SCROLL_VELOCITY = 1500f;
-
-    private final ScrollView mTarget;
-
-    public FolderAutoScrollHelper(ScrollView target) {
-        super(target);
-
-        mTarget = target;
-
-        setActivationDelay(0);
-        setEdgeType(EDGE_TYPE_INSIDE_EXTEND);
-        setExclusive(true);
-        setMaximumVelocity(MAX_SCROLL_VELOCITY, MAX_SCROLL_VELOCITY);
-        setRampDownDuration(0);
-        setRampUpDuration(0);
-    }
-
-    @Override
-    public void scrollTargetBy(int deltaX, int deltaY) {
-        mTarget.scrollBy(deltaX, deltaY);
-    }
-
-    @Override
-    public boolean canTargetScrollHorizontally(int direction) {
-        // List do not scroll horizontally.
-        return false;
-    }
-
-    @Override
-    public boolean canTargetScrollVertically(int direction) {
-        return mTarget.canScrollVertically(direction);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
index a3e8295..f5836c2 100644
--- a/src/com/android/launcher3/FolderIcon.java
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -43,6 +43,7 @@
 
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.FolderInfo.FolderListener;
+import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 
@@ -50,15 +51,15 @@
  * An icon that can appear on in the workspace representing an {@link UserFolder}.
  */
 public class FolderIcon extends FrameLayout implements FolderListener {
-    private Launcher mLauncher;
-    private Folder mFolder;
+    @Thunk Launcher mLauncher;
+    @Thunk Folder mFolder;
     private FolderInfo mInfo;
-    private static boolean sStaticValuesDirty = true;
+    @Thunk static boolean sStaticValuesDirty = true;
 
     private CheckLongPressHelper mLongPressHelper;
 
     // The number of icons to display in the
-    private static final int NUM_ITEMS_IN_PREVIEW = 3;
+    public static final int NUM_ITEMS_IN_PREVIEW = 3;
     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
     private static final int DROP_IN_ANIMATION_DURATION = 400;
     private static final int INITIAL_ITEM_ANIMATION_DURATION = 350;
@@ -88,8 +89,8 @@
 
     public static Drawable sSharedFolderLeaveBehind = null;
 
-    private ImageView mPreviewBackground;
-    private BubbleTextView mFolderName;
+    @Thunk ImageView mPreviewBackground;
+    @Thunk BubbleTextView mFolderName;
 
     FolderRingAnimator mFolderRingAnimator = null;
 
@@ -109,11 +110,11 @@
     private float mSlop;
 
     private PreviewItemDrawingParams mParams = new PreviewItemDrawingParams(0, 0, 0, 0);
-    private PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0);
-    private ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>();
+    @Thunk PreviewItemDrawingParams mAnimParams = new PreviewItemDrawingParams(0, 0, 0, 0);
+    @Thunk ArrayList<ShortcutInfo> mHiddenItems = new ArrayList<ShortcutInfo>();
 
     private Alarm mOpenAlarm = new Alarm();
-    private ItemInfo mDragInfo;
+    @Thunk ItemInfo mDragInfo;
 
     public FolderIcon(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -192,7 +193,7 @@
     public static class FolderRingAnimator {
         public int mCellX;
         public int mCellY;
-        private CellLayout mCellLayout;
+        @Thunk CellLayout mCellLayout;
         public float mOuterRingSize;
         public float mInnerRingSize;
         public FolderIcon mFolderIcon = null;
diff --git a/src/com/android/launcher3/FolderInfo.java b/src/com/android/launcher3/FolderInfo.java
index 85a792f..80b1564 100644
--- a/src/com/android/launcher3/FolderInfo.java
+++ b/src/com/android/launcher3/FolderInfo.java
@@ -29,19 +29,33 @@
  */
 public class FolderInfo extends ItemInfo {
 
+    public static final int NO_FLAGS = 0x00000000;
+
+    /**
+     * The folder is locked in sorted mode
+     */
+    public static final int FLAG_ITEMS_SORTED = 0x00000001;
+
+    /**
+     * It is a work folder
+     */
+    public static final int FLAG_WORK_FOLDER = 0x00000002;
+
     /**
      * Whether this folder has been opened
      */
     boolean opened;
 
+    public int options;
+
     /**
      * The apps and shortcuts
      */
-    ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>();
+    public ArrayList<ShortcutInfo> contents = new ArrayList<ShortcutInfo>();
 
     ArrayList<FolderListener> listeners = new ArrayList<FolderListener>();
 
-    FolderInfo() {
+    public FolderInfo() {
         itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
         user = UserHandleCompat.myUserHandle();
     }
@@ -83,6 +97,8 @@
     void onAddToDatabase(Context context, ContentValues values) {
         super.onAddToDatabase(context, values);
         values.put(LauncherSettings.Favorites.TITLE, title.toString());
+        values.put(LauncherSettings.Favorites.OPTIONS, options);
+
     }
 
     void addListener(FolderListener listener) {
@@ -121,4 +137,25 @@
                 + " cellX=" + cellX + " cellY=" + cellY + " spanX=" + spanX
                 + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos) + ")";
     }
+
+    public boolean hasOption(int optionFlag) {
+        return (options & optionFlag) != 0;
+    }
+
+    /**
+     * @param option flag to set or clear
+     * @param isEnabled whether to set or clear the flag
+     * @param context if not null, save changes to the db.
+     */
+    public void setOption(int option, boolean isEnabled, Context context) {
+        int oldOptions = options;
+        if (isEnabled) {
+            options |= option;
+        } else {
+            options &= ~option;
+        }
+        if (context != null && oldOptions != options) {
+            LauncherModel.updateItemInDatabase(context, this);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java
new file mode 100644
index 0000000..6174892
--- /dev/null
+++ b/src/com/android/launcher3/FolderPagedView.java
@@ -0,0 +1,842 @@
+/**
+ * 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.launcher3;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.Switch;
+
+import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
+import com.android.launcher3.PageIndicator.PageMarkerResources;
+import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.util.Thunk;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class FolderPagedView extends PagedView {
+
+    private static final String TAG = "FolderPagedView";
+
+    private static final boolean ALLOW_FOLDER_SCROLL = true;
+
+    // To enable this flag, user_folder.xml needs to be modified to add sort button.
+    private static final boolean ALLOW_ITEM_SORTING = false;
+
+    private static final int REORDER_ANIMATION_DURATION = 230;
+    private static final int START_VIEW_REORDER_DELAY = 30;
+    private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
+
+    private static final int SPAN_TO_PAGE_DURATION = 350;
+    private static final int SORT_ANIM_HIDE_DURATION = 130;
+    private static final int SORT_ANIM_SHOW_DURATION = 160;
+
+    /**
+     * Fraction of the width to scroll when showing the next page hint.
+     */
+    private static final float SCROLL_HINT_FRACTION = 0.07f;
+
+    private static final int[] sTempPosArray = new int[2];
+
+    // TODO: Remove this restriction
+    private static final int MAX_ITEMS_PER_PAGE = 4;
+
+    public final boolean rtlLayout;
+
+    private final LayoutInflater mInflater;
+    private final IconCache mIconCache;
+
+    @Thunk final HashMap<View, Runnable> mPendingAnimations = new HashMap<>();
+
+    private final int mMaxCountX;
+    private final int mMaxCountY;
+    private final int mMaxItemsPerPage;
+
+    private int mAllocatedContentSize;
+    private int mGridCountX;
+    private int mGridCountY;
+
+    private Folder mFolder;
+    private FocusIndicatorView mFocusIndicatorView;
+    private PagedFolderKeyEventListener mKeyListener;
+
+    private View mSortButton;
+    private Switch mSortSwitch;
+    private View mPageIndicator;
+
+    private boolean mSortOperationPending;
+    boolean mIsSorted;
+
+    public FolderPagedView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LauncherAppState app = LauncherAppState.getInstance();
+        setDataIsReady();
+
+        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
+        if (ALLOW_FOLDER_SCROLL) {
+            mMaxCountX = Math.min((int) grid.numColumns, MAX_ITEMS_PER_PAGE);
+            mMaxCountY = Math.min((int) grid.numRows, MAX_ITEMS_PER_PAGE);
+        } else {
+            mMaxCountX = (int) grid.numColumns;
+            mMaxCountY = (int) grid.numRows;
+        }
+
+        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
+
+        mInflater = LayoutInflater.from(context);
+        mIconCache = app.getIconCache();
+
+        rtlLayout = getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+    }
+
+    public void setFolder(Folder folder) {
+        mFolder = folder;
+        mFocusIndicatorView = (FocusIndicatorView) folder.findViewById(R.id.focus_indicator);
+        mKeyListener = new PagedFolderKeyEventListener(folder);
+        mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
+
+        if (ALLOW_ITEM_SORTING) {
+            // Initialize {@link #mSortSwitch} and {@link #mSortButton}.
+        }
+    }
+
+    /**
+     * Called when sort button is clicked.
+     */
+    private void onSortClicked() {
+        if (mSortOperationPending) {
+            return;
+        }
+        if (mIsSorted) {
+            setIsSorted(false, true);
+        } else {
+            mSortOperationPending = true;
+            doSort();
+        }
+    }
+
+    private void setIsSorted(boolean isSorted, boolean saveChanges) {
+        mIsSorted = isSorted;
+        if (ALLOW_ITEM_SORTING) {
+            mSortSwitch.setChecked(isSorted);
+            mFolder.mInfo.setOption(FolderInfo.FLAG_ITEMS_SORTED, isSorted,
+                    saveChanges ? mFolder.mLauncher : null);
+        }
+    }
+
+    /**
+     * Sorts the contents of the folder and animates the icons on the first page to reflect
+     * the changes.
+     * Steps:
+     *      1. Scroll to first page
+     *      2. Sort all icons in one go
+     *      3. Re-apply the old IconInfos on the first page (so that there is no instant change)
+     *      4. Animate each view individually to reflect the new icon.
+     */
+    private void doSort() {
+        if (!mSortOperationPending) {
+            return;
+        }
+        if (getNextPage() != 0) {
+            snapToPage(0, SPAN_TO_PAGE_DURATION, new DecelerateInterpolator());
+            return;
+        }
+
+        mSortOperationPending = false;
+        ShortcutInfo[][] oldItems = new ShortcutInfo[mGridCountX][mGridCountY];
+        CellLayout currentPage = getCurrentCellLayout();
+        for (int x = 0; x < mGridCountX; x++) {
+            for (int y = 0; y < mGridCountY; y++) {
+                View v = currentPage.getChildAt(x, y);
+                if (v != null) {
+                    oldItems[x][y] = (ShortcutInfo) v.getTag();
+                }
+            }
+        }
+
+        ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder());
+        Collections.sort(views, new ViewComparator());
+        arrangeChildren(views, views.size());
+
+        int delay = 0;
+        float delayAmount = START_VIEW_REORDER_DELAY;
+        final Interpolator hideInterpolator = new DecelerateInterpolator(2);
+        final Interpolator showInterpolator = new OvershootInterpolator(0.8f);
+
+        currentPage = getCurrentCellLayout();
+        for (int x = 0; x < mGridCountX; x++) {
+            for (int y = 0; y < mGridCountY; y++) {
+                final BubbleTextView v = (BubbleTextView) currentPage.getChildAt(x, y);
+                if (v != null) {
+                    final ShortcutInfo info = (ShortcutInfo) v.getTag();
+                    final Runnable clearPending = new Runnable() {
+
+                        @Override
+                        public void run() {
+                            mPendingAnimations.remove(v);
+                            v.setScaleX(1);
+                            v.setScaleY(1);
+                        }
+                    };
+                    if (oldItems[x][y] == null) {
+                        v.setScaleX(0);
+                        v.setScaleY(0);
+                        v.animate().setDuration(SORT_ANIM_SHOW_DURATION)
+                            .setStartDelay(SORT_ANIM_HIDE_DURATION + delay)
+                            .scaleX(1).scaleY(1).setInterpolator(showInterpolator)
+                            .withEndAction(clearPending);
+                        mPendingAnimations.put(v, clearPending);
+                    } else {
+                        // Apply the old iconInfo so that there is no sudden change.
+                        v.applyFromShortcutInfo(oldItems[x][y], mIconCache, false);
+                        v.animate().setStartDelay(delay).setDuration(SORT_ANIM_HIDE_DURATION)
+                            .scaleX(0).scaleY(0)
+                            .setInterpolator(hideInterpolator)
+                            .withEndAction(new Runnable() {
+
+                                @Override
+                                public void run() {
+                                    // Apply the new iconInfo as part of the animation.
+                                    v.applyFromShortcutInfo(info, mIconCache, false);
+                                    v.animate().scaleX(1).scaleY(1)
+                                        .setDuration(SORT_ANIM_SHOW_DURATION).setStartDelay(0)
+                                        .setInterpolator(showInterpolator)
+                                        .withEndAction(clearPending);
+                                }
+                       });
+                       mPendingAnimations.put(v, new Runnable() {
+
+                           @Override
+                           public void run() {
+                               clearPending.run();
+                               v.applyFromShortcutInfo(info, mIconCache, false);
+                           }
+                        });
+                    }
+                    delay += delayAmount;
+                    delayAmount *= VIEW_REORDER_DELAY_FACTOR;
+                }
+            }
+        }
+
+        setIsSorted(true, true);
+    }
+
+    /**
+     * Sets up the grid size such that {@param count} items can fit in the grid.
+     * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
+     * maintaining the restrictions of {@link #mMaxCountX} &amp; {@link #mMaxCountY}.
+     */
+    private void setupContentDimensions(int count) {
+        mAllocatedContentSize = count;
+        boolean done;
+        if (count >= mMaxItemsPerPage) {
+            mGridCountX = mMaxCountX;
+            mGridCountY = mMaxCountY;
+            done = true;
+        } else {
+            done = false;
+        }
+
+        while (!done) {
+            int oldCountX = mGridCountX;
+            int oldCountY = mGridCountY;
+            if (mGridCountX * mGridCountY < count) {
+                // Current grid is too small, expand it
+                if ((mGridCountX <= mGridCountY || mGridCountY == mMaxCountY) && mGridCountX < mMaxCountX) {
+                    mGridCountX++;
+                } else if (mGridCountY < mMaxCountY) {
+                    mGridCountY++;
+                }
+                if (mGridCountY == 0) mGridCountY++;
+            } else if ((mGridCountY - 1) * mGridCountX >= count && mGridCountY >= mGridCountX) {
+                mGridCountY = Math.max(0, mGridCountY - 1);
+            } else if ((mGridCountX - 1) * mGridCountY >= count) {
+                mGridCountX = Math.max(0, mGridCountX - 1);
+            }
+            done = mGridCountX == oldCountX && mGridCountY == oldCountY;
+        }
+
+        // Update grid size
+        for (int i = getPageCount() - 1; i >= 0; i--) {
+            getPageAt(i).setGridSize(mGridCountX, mGridCountY);
+        }
+    }
+
+    /**
+     * Binds items to the layout.
+     * @return list of items that could not be bound, probably because we hit the max size limit.
+     */
+    public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
+        mIsSorted = ALLOW_ITEM_SORTING && mFolder.mInfo.hasOption(FolderInfo.FLAG_ITEMS_SORTED);
+        ArrayList<View> icons = new ArrayList<View>();
+        ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>();
+
+        for (ShortcutInfo item : items) {
+            if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) {
+                extra.add(item);
+            } else {
+                icons.add(createNewView(item));
+            }
+        }
+        arrangeChildren(icons, icons.size(), false);
+        return extra;
+    }
+
+    /**
+     * Create space for a new item at the end, and returns the rank for that item.
+     * Also sets the current page to the last page.
+     */
+    public int allocateRankForNewItem(ShortcutInfo info) {
+        int rank = getItemCount();
+        ArrayList<View> views = new ArrayList<View>(mFolder.getItemsInReadingOrder());
+        if (ALLOW_ITEM_SORTING && mIsSorted) {
+            View tmp = new View(getContext());
+            tmp.setTag(info);
+            int index = Collections.binarySearch(views, tmp, new ViewComparator());
+            if (index < 0) {
+                rank = -index - 1;
+            } else {
+                // Item with same name already exists.
+                // We will just insert it before that item.
+                rank = index;
+            }
+
+        }
+
+        views.add(rank, null);
+        arrangeChildren(views, views.size(), false);
+        setCurrentPage(rank / mMaxItemsPerPage);
+        return rank;
+    }
+
+    public View createAndAddViewForRank(ShortcutInfo item, int rank) {
+        View icon = createNewView(item);
+        addViewForRank(icon, item, rank);
+        return icon;
+    }
+
+    /**
+     * Adds the {@param view} to the layout based on {@param rank} and updated the position
+     * related attributes. It assumes that {@param item} is already attached to the view.
+     */
+    public void addViewForRank(View view, ShortcutInfo item, int rank) {
+        int pagePos = rank % mMaxItemsPerPage;
+        int pageNo = rank / mMaxItemsPerPage;
+
+        item.rank = rank;
+        item.cellX = pagePos % mGridCountX;
+        item.cellY = pagePos / mGridCountX;
+
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
+        lp.cellX = item.cellX;
+        lp.cellY = item.cellY;
+        getPageAt(pageNo).addViewToCellLayout(
+                view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
+    }
+
+    @SuppressLint("InflateParams")
+    private View createNewView(ShortcutInfo item) {
+        final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
+                R.layout.folder_application, null, false);
+        textView.applyFromShortcutInfo(item, mIconCache, false);
+        textView.setOnClickListener(mFolder);
+        textView.setOnLongClickListener(mFolder);
+        textView.setOnFocusChangeListener(mFocusIndicatorView);
+        textView.setOnKeyListener(mKeyListener);
+
+        textView.setLayoutParams(new CellLayout.LayoutParams(
+                item.cellX, item.cellY, item.spanX, item.spanY));
+        return textView;
+    }
+
+    @Override
+    public CellLayout getPageAt(int index) {
+        return (CellLayout) getChildAt(index);
+    }
+
+    public void removeCellLayoutView(View view) {
+        for (int i = getChildCount() - 1; i >= 0; i --) {
+            getPageAt(i).removeView(view);
+        }
+    }
+
+    public CellLayout getCurrentCellLayout() {
+        return getPageAt(getNextPage());
+    }
+
+    private CellLayout createAndAddNewPage() {
+        DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
+        CellLayout page = new CellLayout(getContext());
+        page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
+        page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
+        page.setInvertIfRtl(true);
+        page.setGridSize(mGridCountX, mGridCountY);
+
+        LayoutParams lp = generateDefaultLayoutParams();
+        lp.isFullScreenPage = true;
+        addView(page, -1, lp);
+        return page;
+    }
+
+    public void setFixedSize(int width, int height) {
+        for (int i = getChildCount() - 1; i >= 0; i --) {
+            ((CellLayout) getChildAt(i)).setFixedSize(width, height);
+        }
+    }
+
+    public void removeItem(View v) {
+        for (int i = getChildCount() - 1; i >= 0; i --) {
+            getPageAt(i).removeView(v);
+        }
+    }
+
+    /**
+     * Updates position and rank of all the children in the view.
+     * It essentially removes all views from all the pages and then adds them again in appropriate
+     * page.
+     *
+     * @param list the ordered list of children.
+     * @param itemCount if greater than the total children count, empty spaces are left
+     * at the end, otherwise it is ignored.
+     *
+     */
+    public void arrangeChildren(ArrayList<View> list, int itemCount) {
+        arrangeChildren(list, itemCount, true);
+    }
+
+    private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
+        ArrayList<CellLayout> pages = new ArrayList<CellLayout>();
+        for (int i = 0; i < getChildCount(); i++) {
+            CellLayout page = (CellLayout) getChildAt(i);
+            page.removeAllViews();
+            pages.add(page);
+        }
+        setupContentDimensions(itemCount);
+
+        Iterator<CellLayout> pageItr = pages.iterator();
+        CellLayout currentPage = null;
+
+        int position = 0;
+        int newX, newY, rank;
+
+        boolean isSorted = mIsSorted;
+
+        ViewComparator comparator = new ViewComparator();
+        View lastView = null;
+        rank = 0;
+        for (int i = 0; i < itemCount; i++) {
+            View v = list.size() > i ? list.get(i) : null;
+            if (currentPage == null || position >= mMaxItemsPerPage) {
+                // Next page
+                if (pageItr.hasNext()) {
+                    currentPage = pageItr.next();
+                } else {
+                    currentPage = createAndAddNewPage();
+                }
+                position = 0;
+            }
+
+            if (v != null) {
+                if (lastView != null) {
+                    isSorted &= comparator.compare(lastView, v) <= 0;
+                }
+
+                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
+                newX = position % mGridCountX;
+                newY = position / mGridCountX;
+                ItemInfo info = (ItemInfo) v.getTag();
+                if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
+                    info.cellX = newX;
+                    info.cellY = newY;
+                    info.rank = rank;
+                    if (saveChanges) {
+                        LauncherModel.addOrMoveItemInDatabase(getContext(), info,
+                                mFolder.mInfo.id, 0, info.cellX, info.cellY);
+                    }
+                }
+                lp.cellX = info.cellX;
+                lp.cellY = info.cellY;
+                currentPage.addViewToCellLayout(
+                        v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
+            }
+
+            lastView = v;
+            rank ++;
+            position++;
+        }
+
+        // Remove extra views.
+        boolean removed = false;
+        while (pageItr.hasNext()) {
+            removeView(pageItr.next());
+            removed = true;
+        }
+        if (removed) {
+            setCurrentPage(0);
+        }
+
+        setEnableOverscroll(getPageCount() > 1);
+
+        // Update footer
+        if (ALLOW_ITEM_SORTING) {
+            setIsSorted(isSorted, saveChanges);
+            if (getPageCount() > 1) {
+                mPageIndicator.setVisibility(View.VISIBLE);
+                mSortButton.setVisibility(View.VISIBLE);
+                mFolder.mFolderName.setGravity(rtlLayout ? Gravity.RIGHT : Gravity.LEFT);
+            } else {
+                mPageIndicator.setVisibility(View.GONE);
+                mSortButton.setVisibility(View.GONE);
+                mFolder.mFolderName.setGravity(Gravity.CENTER_HORIZONTAL);
+            }
+        } else {
+            int indicatorVisibility = mPageIndicator.getVisibility();
+            mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
+            if (indicatorVisibility != mPageIndicator.getVisibility()) {
+                mFolder.updateFooterHeight();
+            }
+        }
+    }
+
+    @Override
+    protected void loadAssociatedPages(int page, boolean immediateAndOnly) { }
+
+    @Override
+    public void syncPages() { }
+
+    @Override
+    public void syncPageItems(int page, boolean immediate) { }
+
+    public int getDesiredWidth() {
+        return getPageCount() > 0 ? getPageAt(0).getDesiredWidth() : 0;
+    }
+
+    public int getDesiredHeight()  {
+        return  getPageCount() > 0 ? getPageAt(0).getDesiredHeight() : 0;
+    }
+
+    public int getItemCount() {
+        int lastPageIndex = getChildCount() - 1;
+        if (lastPageIndex < 0) {
+            // If there are no pages, nothing has yet been added to the folder.
+            return 0;
+        }
+        return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount()
+                + lastPageIndex * mMaxItemsPerPage;
+    }
+
+    /**
+     * @return the rank of the cell nearest to the provided pixel position.
+     */
+    public int findNearestArea(int pixelX, int pixelY) {
+        int pageIndex = getNextPage();
+        CellLayout page = getPageAt(pageIndex);
+        page.findNearestArea(pixelX, pixelY, 1, 1, null, false, sTempPosArray);
+        if (mFolder.isLayoutRtl()) {
+            sTempPosArray[0] = page.getCountX() - sTempPosArray[0] - 1;
+        }
+        return Math.min(mAllocatedContentSize - 1,
+                pageIndex * mMaxItemsPerPage + sTempPosArray[1] * mGridCountX + sTempPosArray[0]);
+    }
+
+    @Override
+    protected PageMarkerResources getPageIndicatorMarker(int pageIndex) {
+        return new PageMarkerResources(R.drawable.ic_pageindicator_current_folder,
+                R.drawable.ic_pageindicator_default_folder);
+    }
+
+    public boolean isFull() {
+        return !ALLOW_FOLDER_SCROLL && getItemCount() >= mMaxItemsPerPage;
+    }
+
+    public View getLastItem() {
+        if (getChildCount() < 1) {
+            return null;
+        }
+        ShortcutAndWidgetContainer lastContainer = getCurrentCellLayout().getShortcutsAndWidgets();
+        int lastRank = lastContainer.getChildCount() - 1;
+        if (mGridCountX > 0) {
+            return lastContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX);
+        } else {
+            return lastContainer.getChildAt(lastRank);
+        }
+    }
+
+    /**
+     * Iterates over all its items in a reading order.
+     * @return the view for which the operator returned true.
+     */
+    public View iterateOverItems(ItemOperator op) {
+        for (int k = 0 ; k < getChildCount(); k++) {
+            CellLayout page = getPageAt(k);
+            for (int j = 0; j < page.getCountY(); j++) {
+                for (int i = 0; i < page.getCountX(); i++) {
+                    View v = page.getChildAt(i, j);
+                    if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v, this)) {
+                        return v;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    public String getAccessibilityDescription() {
+        return String.format(getContext().getString(R.string.folder_opened),
+                mGridCountX, mGridCountY);
+    }
+
+    /**
+     * Sets the focus on the first visible child.
+     */
+    public void setFocusOnFirstChild() {
+        View firstChild = getCurrentCellLayout().getChildAt(0, 0);
+        if (firstChild != null) {
+            firstChild.requestFocus();
+        }
+    }
+
+    @Override
+    protected void notifyPageSwitchListener() {
+        super.notifyPageSwitchListener();
+        if (mFolder != null) {
+            mFolder.updateTextViewFocus();
+        }
+        if (ALLOW_ITEM_SORTING && mSortOperationPending && getNextPage() == 0) {
+            post(new Runnable() {
+
+                @Override
+                public void run() {
+                    if (mSortOperationPending) {
+                        doSort();
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Scrolls the current view by a fraction
+     */
+    public void showScrollHint(int direction) {
+        float fraction = (direction == DragController.SCROLL_LEFT) ^ rtlLayout
+                ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION;
+        int hint = (int) (fraction * getWidth());
+        int scroll = getScrollForPage(getNextPage()) + hint;
+        int delta = scroll - mUnboundedScrollX;
+        if (delta != 0) {
+            mScroller.setInterpolator(new DecelerateInterpolator());
+            mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, Folder.SCROLL_HINT_DURATION);
+            invalidate();
+        }
+    }
+
+    public void clearScrollHint() {
+        if (mUnboundedScrollX != getScrollForPage(getNextPage())) {
+            snapToPage(getNextPage());
+        }
+    }
+
+    /**
+     * Finish animation all the views which are animating across pages
+     */
+    public void completePendingPageChanges() {
+        if (!mPendingAnimations.isEmpty()) {
+            HashMap<View, Runnable> pendingViews = new HashMap<>(mPendingAnimations);
+            for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) {
+                e.getKey().animate().cancel();
+                e.getValue().run();
+            }
+        }
+    }
+
+    public boolean rankOnCurrentPage(int rank) {
+        int p = rank / mMaxItemsPerPage;
+        return p == getNextPage();
+    }
+
+    @Override
+    protected void onPageBeginMoving() {
+        super.onPageBeginMoving();
+        getVisiblePages(sTempPosArray);
+        for (int i = sTempPosArray[0]; i <= sTempPosArray[1]; i++) {
+            verifyVisibleHighResIcons(i);
+        }
+    }
+
+    /**
+     * Ensures that all the icons on the given page are of high-res
+     */
+    public void verifyVisibleHighResIcons(int pageNo) {
+        CellLayout page = getPageAt(pageNo);
+        if (page != null) {
+            ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
+            for (int i = parent.getChildCount() - 1; i >= 0; i--) {
+                ((BubbleTextView) parent.getChildAt(i)).verifyHighRes();
+            }
+        }
+    }
+
+    /**
+     * Reorders the items such that the {@param empty} spot moves to {@param target}
+     */
+    public void realTimeReorder(int empty, int target) {
+        completePendingPageChanges();
+        int delay = 0;
+        float delayAmount = START_VIEW_REORDER_DELAY;
+
+        // Animation only happens on the current page.
+        int pageToAnimate = getNextPage();
+
+        int pageT = target / mMaxItemsPerPage;
+        int pagePosT = target % mMaxItemsPerPage;
+
+        if (pageT != pageToAnimate) {
+            Log.e(TAG, "Cannot animate when the target cell is invisible");
+        }
+        int pagePosE = empty % mMaxItemsPerPage;
+        int pageE = empty / mMaxItemsPerPage;
+
+        int startPos, endPos;
+        int moveStart, moveEnd;
+        int direction;
+
+        if (target == empty) {
+            // No animation
+            return;
+        } else if (target > empty) {
+            // Items will move backwards to make room for the empty cell.
+            direction = 1;
+
+            // If empty cell is in a different page, move them instantly.
+            if (pageE < pageToAnimate) {
+                moveStart = empty;
+                // Instantly move the first item in the current page.
+                moveEnd = pageToAnimate * mMaxItemsPerPage;
+                // Animate the 2nd item in the current page, as the first item was already moved to
+                // the last page.
+                startPos = 0;
+            } else {
+                moveStart = moveEnd = -1;
+                startPos = pagePosE;
+            }
+
+            endPos = pagePosT;
+        } else {
+            // The items will move forward.
+            direction = -1;
+
+            if (pageE > pageToAnimate) {
+                // Move the items immediately.
+                moveStart = empty;
+                // Instantly move the last item in the current page.
+                moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1;
+
+                // Animations start with the second last item in the page
+                startPos = mMaxItemsPerPage - 1;
+            } else {
+                moveStart = moveEnd = -1;
+                startPos = pagePosE;
+            }
+
+            endPos = pagePosT;
+        }
+
+        // Instant moving views.
+        while (moveStart != moveEnd) {
+            int rankToMove = moveStart + direction;
+            int p = rankToMove / mMaxItemsPerPage;
+            int pagePos = rankToMove % mMaxItemsPerPage;
+            int x = pagePos % mGridCountX;
+            int y = pagePos / mGridCountX;
+
+            final CellLayout page = getPageAt(p);
+            final View v = page.getChildAt(x, y);
+            if (v != null) {
+                if (pageToAnimate != p) {
+                    page.removeView(v);
+                    addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart);
+                } else {
+                    // Do a fake animation before removing it.
+                    final int newRank = moveStart;
+                    final float oldTranslateX = v.getTranslationX();
+
+                    Runnable endAction = new Runnable() {
+
+                        @Override
+                        public void run() {
+                            mPendingAnimations.remove(v);
+                            v.setTranslationX(oldTranslateX);
+                            ((CellLayout) v.getParent().getParent()).removeView(v);
+                            addViewForRank(v, (ShortcutInfo) v.getTag(), newRank);
+                        }
+                    };
+                    v.animate()
+                        .translationXBy((direction > 0 ^ rtlLayout) ? -v.getWidth() : v.getWidth())
+                        .setDuration(REORDER_ANIMATION_DURATION)
+                        .setStartDelay(0)
+                        .withEndAction(endAction);
+                    mPendingAnimations.put(v, endAction);
+                }
+            }
+            moveStart = rankToMove;
+        }
+
+        if ((endPos - startPos) * direction <= 0) {
+            // No animation
+            return;
+        }
+
+        CellLayout page = getPageAt(pageToAnimate);
+        for (int i = startPos; i != endPos; i += direction) {
+            int nextPos = i + direction;
+            View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
+            if (v != null) {
+                ((ItemInfo) v.getTag()).rank -= direction;
+            }
+            if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
+                    REORDER_ANIMATION_DURATION, delay, true, true)) {
+                delay += delayAmount;
+                delayAmount *= VIEW_REORDER_DELAY_FACTOR;
+            }
+        }
+    }
+
+    private static class ViewComparator implements Comparator<View> {
+        private final Collator mCollator = Collator.getInstance();
+
+        @Override
+        public int compare(View lhs, View rhs) {
+            return mCollator.compare( ((ShortcutInfo) lhs.getTag()).title.toString(),
+                    ((ShortcutInfo) rhs.getTag()).title.toString());
+        }
+    }
+}
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index b08272f..b614bc6 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,24 +16,18 @@
 
 package com.android.launcher3;
 
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
-import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
-import java.util.ArrayList;
-
 public class Hotseat extends FrameLayout {
-    private static final String TAG = "Hotseat";
 
     private CellLayout mContent;
 
@@ -86,19 +80,22 @@
     int getOrderInHotseat(int x, int y) {
         return hasVerticalHotseat() ? (mContent.getCountY() - y - 1) : x;
     }
+
     /* Get the orientation specific coordinates given an invariant order in the hotseat. */
     int getCellXFromOrder(int rank) {
         return hasVerticalHotseat() ? 0 : rank;
     }
+
     int getCellYFromOrder(int rank) {
         return hasVerticalHotseat() ? (mContent.getCountY() - (rank + 1)) : 0;
     }
+
+    public int getAllAppsButtonRank() {
+        return mAllAppsButtonRank;
+    }
+
     public boolean isAllAppsButtonRank(int rank) {
-        if (LauncherAppState.isDisableAllApps()) {
-            return false;
-        } else {
-            return rank == mAllAppsButtonRank;
-        }
+        return rank == mAllAppsButtonRank;
     }
 
     /** This returns the coordinates of an app in a given cell, relative to the DragLayer */
@@ -141,35 +138,33 @@
     void resetLayout() {
         mContent.removeAllViewsInLayout();
 
-        if (!LauncherAppState.isDisableAllApps()) {
-            // Add the Apps button
-            Context context = getContext();
+        // Add the Apps button
+        Context context = getContext();
 
-            LayoutInflater inflater = LayoutInflater.from(context);
-            TextView allAppsButton = (TextView)
-                    inflater.inflate(R.layout.all_apps_button, mContent, false);
-            Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon);
+        LayoutInflater inflater = LayoutInflater.from(context);
+        TextView allAppsButton = (TextView)
+                inflater.inflate(R.layout.all_apps_button, mContent, false);
+        Drawable d = context.getResources().getDrawable(R.drawable.all_apps_button_icon);
 
-            Utilities.resizeIconDrawable(d);
-            allAppsButton.setCompoundDrawables(null, d, null, null);
+        Utilities.resizeIconDrawable(d);
+        allAppsButton.setCompoundDrawables(null, d, null, null);
 
-            allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label));
-            allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener());
-            if (mLauncher != null) {
-                allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener());
-                mLauncher.setAllAppsButton(allAppsButton);
-                allAppsButton.setOnClickListener(mLauncher);
-                allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler);
-            }
-
-            // Note: We do this to ensure that the hotseat is always laid out in the orientation of
-            // the hotseat in order regardless of which orientation they were added
-            int x = getCellXFromOrder(mAllAppsButtonRank);
-            int y = getCellYFromOrder(mAllAppsButtonRank);
-            CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1);
-            lp.canReorder = false;
-            mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true);
+        allAppsButton.setContentDescription(context.getString(R.string.all_apps_button_label));
+        allAppsButton.setOnKeyListener(new HotseatIconKeyEventListener());
+        if (mLauncher != null) {
+            allAppsButton.setOnTouchListener(mLauncher.getHapticFeedbackTouchListener());
+            mLauncher.setAllAppsButton(allAppsButton);
+            allAppsButton.setOnClickListener(mLauncher);
+            allAppsButton.setOnFocusChangeListener(mLauncher.mFocusHandler);
         }
+
+        // Note: We do this to ensure that the hotseat is always laid out in the orientation of
+        // the hotseat in order regardless of which orientation they were added
+        int x = getCellXFromOrder(mAllAppsButtonRank);
+        int y = getCellYFromOrder(mAllAppsButtonRank);
+        CellLayout.LayoutParams lp = new CellLayout.LayoutParams(x,y,1,1);
+        lp.canReorder = false;
+        mContent.addViewToCellLayout(allAppsButton, -1, allAppsButton.getId(), lp, true);
     }
 
     @Override
@@ -181,55 +176,4 @@
         }
         return false;
     }
-
-    void addAllAppsFolder(IconCache iconCache,
-            ArrayList<AppInfo> allApps, ArrayList<ComponentName> onWorkspace,
-            Launcher launcher, Workspace workspace) {
-        if (LauncherAppState.isDisableAllApps()) {
-            FolderInfo fi = new FolderInfo();
-
-            fi.cellX = getCellXFromOrder(mAllAppsButtonRank);
-            fi.cellY = getCellYFromOrder(mAllAppsButtonRank);
-            fi.spanX = 1;
-            fi.spanY = 1;
-            fi.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT;
-            fi.screenId = mAllAppsButtonRank;
-            fi.itemType = LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
-            fi.title = "More Apps";
-            LauncherModel.addItemToDatabase(launcher, fi, fi.container, fi.screenId, fi.cellX,
-                    fi.cellY, false);
-            FolderIcon folder = FolderIcon.fromXml(R.layout.folder_icon, launcher,
-                    getLayout(), fi, iconCache);
-            workspace.addInScreen(folder, fi.container, fi.screenId, fi.cellX, fi.cellY,
-                    fi.spanX, fi.spanY);
-
-            for (AppInfo info: allApps) {
-                ComponentName cn = info.intent.getComponent();
-                if (!onWorkspace.contains(cn)) {
-                    Log.d(TAG, "Adding to 'more apps': " + info.intent);
-                    ShortcutInfo si = info.makeShortcut();
-                    fi.add(si);
-                }
-            }
-        }
-    }
-
-    void addAppsToAllAppsFolder(ArrayList<AppInfo> apps) {
-        if (LauncherAppState.isDisableAllApps()) {
-            View v = mContent.getChildAt(getCellXFromOrder(mAllAppsButtonRank), getCellYFromOrder(mAllAppsButtonRank));
-            FolderIcon fi = null;
-
-            if (v instanceof FolderIcon) {
-                fi = (FolderIcon) v;
-            } else {
-                return;
-            }
-
-            FolderInfo info = fi.getFolderInfo();
-            for (AppInfo a: apps) {
-                ShortcutInfo si = a.makeShortcut();
-                info.add(si);
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 5a0875b..48b38f1 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -18,17 +18,23 @@
 
 import android.app.ActivityManager;
 import android.content.ComponentName;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
+import android.os.Handler;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -36,16 +42,15 @@
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.PackageItemInfo;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map.Entry;
 
 /**
@@ -56,49 +61,34 @@
     private static final String TAG = "Launcher.IconCache";
 
     private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
-    private static final String RESOURCE_FILE_PREFIX = "icon_";
 
     // Empty class name is used for storing package default entry.
     private static final String EMPTY_CLASS_NAME = ".";
 
     private static final boolean DEBUG = false;
 
-    private static class CacheEntry {
+    private static final int LOW_RES_SCALE_FACTOR = 8;
+
+    @Thunk static class CacheEntry {
         public Bitmap icon;
         public CharSequence title;
         public CharSequence contentDescription;
+        public boolean isLowResIcon;
     }
 
-    private static class CacheKey {
-        public ComponentName componentName;
-        public UserHandleCompat user;
+    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>();
+    private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
 
-        CacheKey(ComponentName componentName, UserHandleCompat user) {
-            this.componentName = componentName;
-            this.user = user;
-        }
-
-        @Override
-        public int hashCode() {
-            return componentName.hashCode() + user.hashCode();
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            CacheKey other = (CacheKey) o;
-            return other.componentName.equals(componentName) && other.user.equals(user);
-        }
-    }
-
-    private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons =
-            new HashMap<UserHandleCompat, Bitmap>();
     private final Context mContext;
     private final PackageManager mPackageManager;
     private final UserManagerCompat mUserManager;
     private final LauncherAppsCompat mLauncherApps;
-    private final HashMap<CacheKey, CacheEntry> mCache =
-            new HashMap<CacheKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
-    private int mIconDpi;
+    private final HashMap<ComponentKey, CacheEntry> mCache =
+            new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
+    private final int mIconDpi;
+    private final IconDB mIconDb;
+
+    private final Handler mWorkerHandler;
 
     public IconCache(Context context) {
         ActivityManager activityManager =
@@ -109,13 +99,12 @@
         mUserManager = UserManagerCompat.getInstance(mContext);
         mLauncherApps = LauncherAppsCompat.getInstance(mContext);
         mIconDpi = activityManager.getLauncherLargeIconDensity();
+        mIconDb = new IconDB(context);
 
-        // need to set mIconDpi before getting default icon
-        UserHandleCompat myUser = UserHandleCompat.myUserHandle();
-        mDefaultIcons.put(myUser, makeDefaultIcon(myUser));
+        mWorkerHandler = new Handler(LauncherModel.sWorkerThread.getLooper());
     }
 
-    public Drawable getFullResDefaultActivityIcon() {
+    private Drawable getFullResDefaultActivityIcon() {
         return getFullResIcon(Resources.getSystem(), android.R.mipmap.sym_def_app_icon);
     }
 
@@ -184,26 +173,171 @@
      * Remove any records for the supplied ComponentName.
      */
     public synchronized void remove(ComponentName componentName, UserHandleCompat user) {
-        mCache.remove(new CacheKey(componentName, user));
+        mCache.remove(new ComponentKey(componentName, user));
     }
 
     /**
-     * Remove any records for the supplied package name.
+     * Remove any records for the supplied package name from memory.
      */
-    public synchronized void remove(String packageName, UserHandleCompat user) {
-        HashSet<CacheKey> forDeletion = new HashSet<CacheKey>();
-        for (CacheKey key: mCache.keySet()) {
+    private void removeFromMemCacheLocked(String packageName, UserHandleCompat user) {
+        HashSet<ComponentKey> forDeletion = new HashSet<ComponentKey>();
+        for (ComponentKey key: mCache.keySet()) {
             if (key.componentName.getPackageName().equals(packageName)
                     && key.user.equals(user)) {
                 forDeletion.add(key);
             }
         }
-        for (CacheKey condemned: forDeletion) {
+        for (ComponentKey condemned: forDeletion) {
             mCache.remove(condemned);
         }
     }
 
     /**
+     * Updates the entries related to the given package in memory and persistent DB.
+     */
+    public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) {
+        removeIconsForPkg(packageName, user);
+        try {
+            PackageInfo info = mPackageManager.getPackageInfo(packageName,
+                    PackageManager.GET_UNINSTALLED_PACKAGES);
+            long userSerial = mUserManager.getSerialNumberForUser(user);
+            for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) {
+                addIconToDBAndMemCache(app, info, userSerial);
+            }
+        } catch (NameNotFoundException e) {
+            Log.d(TAG, "Package not found", e);
+            return;
+        }
+    }
+
+    /**
+     * Removes the entries related to the given package in memory and persistent DB.
+     */
+    public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) {
+        removeFromMemCacheLocked(packageName, user);
+        long userSerial = mUserManager.getSerialNumberForUser(user);
+        mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
+                IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
+                new String[] {packageName + "/%", Long.toString(userSerial)});
+    }
+
+    /**
+     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+     * the DB and are updated.
+     * @return The set of packages for which icons have updated.
+     */
+    public HashSet<String> updateDBIcons(UserHandleCompat user, List<LauncherActivityInfoCompat> apps) {
+        mIconDb.updateSystemStateString(mContext);
+        long userSerial = mUserManager.getSerialNumberForUser(user);
+        PackageManager pm = mContext.getPackageManager();
+        HashMap<String, PackageInfo> pkgInfoMap = new HashMap<String, PackageInfo>();
+        for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES)) {
+            pkgInfoMap.put(info.packageName, info);
+        }
+
+        HashMap<ComponentName, LauncherActivityInfoCompat> componentMap = new HashMap<>();
+        for (LauncherActivityInfoCompat app : apps) {
+            componentMap.put(app.getComponentName(), app);
+        }
+
+        Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
+                new String[] {IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
+                    IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
+                    IconDB.COLUMN_SYSTEM_STATE},
+                IconDB.COLUMN_USER + " = ? ",
+                new String[] {Long.toString(userSerial)},
+                null, null, null);
+
+        final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
+        final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
+        final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
+        final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
+        final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
+
+        HashSet<Integer> itemsToRemove = new HashSet<Integer>();
+        HashSet<String> updatedPackages = new HashSet<String>();
+
+        while (c.moveToNext()) {
+            String cn = c.getString(indexComponent);
+            ComponentName component = ComponentName.unflattenFromString(cn);
+            PackageInfo info = pkgInfoMap.get(component.getPackageName());
+            if (info == null) {
+                itemsToRemove.add(c.getInt(rowIndex));
+                continue;
+            }
+            if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
+                // Application is not present
+                continue;
+            }
+
+            long updateTime = c.getLong(indexLastUpdate);
+            int version = c.getInt(indexVersion);
+            LauncherActivityInfoCompat app = componentMap.remove(component);
+            if (version == info.versionCode && updateTime == info.lastUpdateTime &&
+                    TextUtils.equals(mIconDb.mSystemState, c.getString(systemStateIndex))) {
+                continue;
+            }
+            if (app == null) {
+                itemsToRemove.add(c.getInt(rowIndex));
+                continue;
+            }
+            ContentValues values = updateCacheAndGetContentValues(app);
+            mIconDb.getWritableDatabase().update(IconDB.TABLE_NAME, values,
+                    IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
+                    new String[] {cn, Long.toString(userSerial)});
+
+            updatedPackages.add(component.getPackageName());
+        }
+        c.close();
+        if (!itemsToRemove.isEmpty()) {
+            mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
+                    IconDB.COLUMN_ROWID + " IN ( " + TextUtils.join(", ", itemsToRemove) +" )",
+                    null);
+        }
+
+        // Insert remaining apps.
+        for (LauncherActivityInfoCompat app : componentMap.values()) {
+            PackageInfo info = pkgInfoMap.get(app.getComponentName().getPackageName());
+            if (info == null) {
+                continue;
+            }
+            addIconToDBAndMemCache(app, info, userSerial);
+        }
+        return updatedPackages;
+    }
+
+    private void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info,
+            long userSerial) {
+        ContentValues values = updateCacheAndGetContentValues(app);
+        addIconToDB(values, app.getComponentName(), info, userSerial);
+        values.put(IconDB.COLUMN_COMPONENT, app.getComponentName().flattenToString());
+    }
+
+    /**
+     * Updates {@param values} to contain versoning information and adds it to the DB.
+     * @param values {@link ContentValues} containing icon & title
+     */
+    private void addIconToDB(ContentValues values, ComponentName key,
+            PackageInfo info, long userSerial) {
+        values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
+        values.put(IconDB.COLUMN_USER, userSerial);
+        values.put(IconDB.COLUMN_LAST_UPDATED, info.lastUpdateTime);
+        values.put(IconDB.COLUMN_VERSION, info.versionCode);
+        mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
+                SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
+    private ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app) {
+        CacheEntry entry = new CacheEntry();
+        entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext);
+        entry.title = app.getLabel();
+        entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser());
+        mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry);
+
+        return mIconDb.newContentValues(entry.icon, entry.title.toString());
+    }
+
+    /**
      * Empty out the cache.
      */
     public synchronized void flush() {
@@ -214,7 +348,7 @@
      * Empty out the cache that aren't of the correct grid size
      */
     public synchronized void flushInvalidIcons(DeviceProfile grid) {
-        Iterator<Entry<CacheKey, CacheEntry>> it = mCache.entrySet().iterator();
+        Iterator<Entry<ComponentKey, CacheEntry>> it = mCache.entrySet().iterator();
         while (it.hasNext()) {
             final CacheEntry e = it.next().getValue();
             if ((e.icon != null) && (e.icon.getWidth() < grid.iconSizePx
@@ -225,18 +359,52 @@
     }
 
     /**
+     * Fetches high-res icon for the provided ItemInfo and updates the caller when done.
+     * @return a request ID that can be used to cancel the request.
+     */
+    public IconLoadRequest updateIconInBackground(final BubbleTextView caller, final ItemInfo info) {
+        Runnable request = new Runnable() {
+
+            @Override
+            public void run() {
+                if (info instanceof AppInfo) {
+                    getTitleAndIcon((AppInfo) info, null, false);
+                } else if (info instanceof ShortcutInfo) {
+                    ShortcutInfo st = (ShortcutInfo) info;
+                    getTitleAndIcon(st,
+                            st.promisedIntent != null ? st.promisedIntent : st.intent,
+                            st.user, false);
+                }
+                mMainThreadExecutor.execute(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        caller.reapplyItemInfo(info);
+                    }
+                });
+            }
+        };
+        mWorkerHandler.post(request);
+        return new IconLoadRequest(request, mWorkerHandler);
+    }
+
+    /**
      * Fill in "application" with the icon and label for "info."
      */
-    public synchronized void getTitleAndIcon(AppInfo application, LauncherActivityInfoCompat info,
-            HashMap<Object, CharSequence> labelCache) {
-        CacheEntry entry = cacheLocked(application.componentName, info, labelCache,
-                info.getUser(), false);
-
+    public synchronized void getTitleAndIcon(AppInfo application,
+            LauncherActivityInfoCompat info, boolean useLowResIcon) {
+        CacheEntry entry = cacheLocked(application.componentName, info,
+                info == null ? application.user : info.getUser(),
+                false, useLowResIcon);
         application.title = entry.title;
         application.iconBitmap = entry.icon;
         application.contentDescription = entry.contentDescription;
+        application.usingLowResIcon = entry.isLowResIcon;
     }
 
+    /**
+     * Returns a high res icon for the given intent and user
+     */
     public synchronized Bitmap getIcon(Intent intent, UserHandleCompat user) {
         ComponentName component = intent.getComponent();
         // null info means not installed, but if we have a component from the intent then
@@ -246,15 +414,16 @@
         }
 
         LauncherActivityInfoCompat launcherActInfo = mLauncherApps.resolveActivity(intent, user);
-        CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, true);
+        CacheEntry entry = cacheLocked(component, launcherActInfo, user, true, true);
         return entry.icon;
     }
 
     /**
-     * Fill in "shortcutInfo" with the icon and label for "info."
+     * Fill in {@param shortcutInfo} with the icon and label for {@param intent}. If the
+     * corresponding activity is not found, it reverts to the package icon.
      */
     public synchronized void getTitleAndIcon(ShortcutInfo shortcutInfo, Intent intent,
-            UserHandleCompat user, boolean usePkgIcon) {
+            UserHandleCompat user, boolean useLowResIcon) {
         ComponentName component = intent.getComponent();
         // null info means not installed, but if we have a component from the intent then
         // we should still look in the cache for restored app icons.
@@ -262,17 +431,38 @@
             shortcutInfo.setIcon(getDefaultIcon(user));
             shortcutInfo.title = "";
             shortcutInfo.usingFallbackIcon = true;
+            shortcutInfo.usingLowResIcon = false;
         } else {
-            LauncherActivityInfoCompat launcherActInfo =
-                    mLauncherApps.resolveActivity(intent, user);
-            CacheEntry entry = cacheLocked(component, launcherActInfo, null, user, usePkgIcon);
-
-            shortcutInfo.setIcon(entry.icon);
-            shortcutInfo.title = entry.title;
-            shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
+            LauncherActivityInfoCompat info = mLauncherApps.resolveActivity(intent, user);
+            getTitleAndIcon(shortcutInfo, component, info, user, true, useLowResIcon);
         }
     }
 
+    /**
+     * Fill in {@param shortcutInfo} with the icon and label for {@param info}
+     */
+    public synchronized void getTitleAndIcon(
+            ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info,
+            UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) {
+        CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon);
+        shortcutInfo.setIcon(entry.icon);
+        shortcutInfo.title = entry.title;
+        shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
+        shortcutInfo.usingLowResIcon = entry.isLowResIcon;
+    }
+
+    /**
+     * Fill in {@param appInfo} with the icon and label for {@param packageName}
+     */
+    public synchronized void getTitleAndIconForApp(
+            String packageName, UserHandleCompat user, boolean useLowResIcon,
+            PackageItemInfo infoOut) {
+        CacheEntry entry = getEntryForPackageLocked(packageName, user, useLowResIcon);
+        infoOut.iconBitmap = entry.icon;
+        infoOut.title = entry.title;
+        infoOut.usingLowResIcon = entry.isLowResIcon;
+        infoOut.contentDescription = entry.contentDescription;
+    }
 
     public synchronized Bitmap getDefaultIcon(UserHandleCompat user) {
         if (!mDefaultIcons.containsKey(user)) {
@@ -281,16 +471,6 @@
         return mDefaultIcons.get(user);
     }
 
-    public synchronized Bitmap getIcon(ComponentName component, LauncherActivityInfoCompat info,
-            HashMap<Object, CharSequence> labelCache) {
-        if (info == null || component == null) {
-            return null;
-        }
-
-        CacheEntry entry = cacheLocked(component, info, labelCache, info.getUser(), false);
-        return entry.icon;
-    }
-
     public boolean isDefaultIcon(Bitmap icon, UserHandleCompat user) {
         return mDefaultIcons.get(user) == icon;
     }
@@ -300,44 +480,27 @@
      * This method is not thread safe, it must be called from a synchronized method.
      */
     private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
-            HashMap<Object, CharSequence> labelCache, UserHandleCompat user, boolean usePackageIcon) {
-        CacheKey cacheKey = new CacheKey(componentName, user);
+            UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
+        ComponentKey cacheKey = new ComponentKey(componentName, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null) {
+        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
             entry = new CacheEntry();
-
             mCache.put(cacheKey, entry);
 
-            if (info != null) {
-                ComponentName labelKey = info.getComponentName();
-                if (labelCache != null && labelCache.containsKey(labelKey)) {
-                    entry.title = labelCache.get(labelKey).toString();
-                } else {
-                    entry.title = info.getLabel().toString();
-                    if (labelCache != null) {
-                        labelCache.put(labelKey, entry.title);
-                    }
-                }
-
-                entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
-                entry.icon = Utilities.createIconBitmap(
-                        info.getBadgedIcon(mIconDpi), mContext);
-            } else {
-                entry.title = "";
-                Bitmap preloaded = getPreloadedIcon(componentName, user);
-                if (preloaded != null) {
-                    if (DEBUG) Log.d(TAG, "using preloaded icon for " +
-                            componentName.toShortString());
-                    entry.icon = preloaded;
+            // Check the DB first.
+            if (!getEntryFromDB(componentName, user, entry, useLowResIcon)) {
+                if (info != null) {
+                    entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext);
                 } else {
                     if (usePackageIcon) {
-                        CacheEntry packageEntry = getEntryForPackage(
-                                componentName.getPackageName(), user);
+                        CacheEntry packageEntry = getEntryForPackageLocked(
+                                componentName.getPackageName(), user, false);
                         if (packageEntry != null) {
                             if (DEBUG) Log.d(TAG, "using package default icon for " +
                                     componentName.toShortString());
                             entry.icon = packageEntry.icon;
                             entry.title = packageEntry.title;
+                            entry.contentDescription = packageEntry.contentDescription;
                         }
                     }
                     if (entry.icon == null) {
@@ -347,6 +510,11 @@
                     }
                 }
             }
+
+            if (TextUtils.isEmpty(entry.title) && info != null) {
+                entry.title = info.getLabel().toString();
+                entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
+            }
         }
         return entry;
     }
@@ -357,9 +525,9 @@
      */
     public synchronized void cachePackageInstallInfo(String packageName, UserHandleCompat user,
             Bitmap icon, CharSequence title) {
-        remove(packageName, user);
+        removeFromMemCacheLocked(packageName, user);
 
-        CacheEntry entry = getEntryForPackage(packageName, user);
+        CacheEntry entry = getEntryForPackageLocked(packageName, user, false);
         if (!TextUtils.isEmpty(title)) {
             entry.title = title;
         }
@@ -371,56 +539,61 @@
     /**
      * Gets an entry for the package, which can be used as a fallback entry for various components.
      * This method is not thread safe, it must be called from a synchronized method.
+     *
      */
-    private CacheEntry getEntryForPackage(String packageName, UserHandleCompat user) {
-        ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);;
-        CacheKey cacheKey = new CacheKey(cn, user);
+    private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user,
+            boolean useLowResIcon) {
+        ComponentName cn = new ComponentName(packageName, EMPTY_CLASS_NAME);
+        ComponentKey cacheKey = new ComponentKey(cn, user);
         CacheEntry entry = mCache.get(cacheKey);
-        if (entry == null) {
+        if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
             entry = new CacheEntry();
-            entry.title = "";
             mCache.put(cacheKey, entry);
 
-            try {
-                ApplicationInfo info = mPackageManager.getApplicationInfo(packageName, 0);
-                entry.title = info.loadLabel(mPackageManager);
-                entry.icon = Utilities.createIconBitmap(info.loadIcon(mPackageManager), mContext);
-            } catch (NameNotFoundException e) {
-                if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
-            }
+            // Check the DB first.
+            if (!getEntryFromDB(cn, user, entry, useLowResIcon)) {
+                try {
+                    PackageInfo info = mPackageManager.getPackageInfo(packageName, 0);
+                    ApplicationInfo appInfo = info.applicationInfo;
+                    if (appInfo == null) {
+                        throw new NameNotFoundException("ApplicationInfo is null");
+                    }
+                    Drawable drawable = mUserManager.getBadgedDrawableForUser(
+                            appInfo.loadIcon(mPackageManager), user);
+                    entry.icon = Utilities.createIconBitmap(drawable, mContext);
+                    entry.title = appInfo.loadLabel(mPackageManager);
+                    entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
+                    entry.isLowResIcon = false;
 
-            if (entry.icon == null) {
-                entry.icon = getPreloadedIcon(cn, user);
+                    // Add the icon in the DB here, since these do not get written during
+                    // package updates.
+                    ContentValues values =
+                            mIconDb.newContentValues(entry.icon, entry.title.toString());
+                    addIconToDB(values, cn, info, mUserManager.getSerialNumberForUser(user));
+
+                } catch (NameNotFoundException e) {
+                    if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
+                }
             }
         }
         return entry;
     }
 
-    public synchronized HashMap<ComponentName,Bitmap> getAllIcons() {
-        HashMap<ComponentName,Bitmap> set = new HashMap<ComponentName,Bitmap>();
-        for (CacheKey ck : mCache.keySet()) {
-            final CacheEntry e = mCache.get(ck);
-            set.put(ck.componentName, e.icon);
-        }
-        return set;
-    }
-
     /**
      * Pre-load an icon into the persistent cache.
      *
      * <P>Queries for a component that does not exist in the package manager
      * will be answered by the persistent cache.
      *
-     * @param context application context
      * @param componentName the icon should be returned for this component
      * @param icon the icon to be persisted
      * @param dpi the native density of the icon
      */
-    public static void preloadIcon(Context context, ComponentName componentName, Bitmap icon,
-            int dpi) {
+    public void preloadIcon(ComponentName componentName, Bitmap icon, int dpi, String label,
+            long userSerial) {
         // TODO rescale to the correct native DPI
         try {
-            PackageManager packageManager = context.getPackageManager();
+            PackageManager packageManager = mContext.getPackageManager();
             packageManager.getActivityIcon(componentName);
             // component is present on the system already, do nothing
             return;
@@ -428,100 +601,135 @@
             // pass
         }
 
-        final String key = componentName.flattenToString();
-        FileOutputStream resourceFile = null;
+        ContentValues values = mIconDb.newContentValues(icon, label);
+        values.put(IconDB.COLUMN_COMPONENT, componentName.flattenToString());
+        values.put(IconDB.COLUMN_USER, userSerial);
+        mIconDb.getWritableDatabase().insertWithOnConflict(IconDB.TABLE_NAME, null, values,
+                SQLiteDatabase.CONFLICT_REPLACE);
+    }
+
+    private boolean getEntryFromDB(ComponentName component, UserHandleCompat user,
+            CacheEntry entry, boolean lowRes) {
+        Cursor c = mIconDb.getReadableDatabase().query(IconDB.TABLE_NAME,
+                new String[] {lowRes ? IconDB.COLUMN_ICON_LOW_RES : IconDB.COLUMN_ICON,
+                        IconDB.COLUMN_LABEL},
+                IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
+                new String[] {component.flattenToString(),
+                    Long.toString(mUserManager.getSerialNumberForUser(user))},
+                null, null, null);
         try {
-            resourceFile = context.openFileOutput(getResourceFilename(componentName),
-                    Context.MODE_PRIVATE);
-            ByteArrayOutputStream os = new ByteArrayOutputStream();
-            if (icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 75, os)) {
-                byte[] buffer = os.toByteArray();
-                resourceFile.write(buffer, 0, buffer.length);
-            } else {
-                Log.w(TAG, "failed to encode cache for " + key);
-                return;
-            }
-        } catch (FileNotFoundException e) {
-            Log.w(TAG, "failed to pre-load cache for " + key, e);
-        } catch (IOException e) {
-            Log.w(TAG, "failed to pre-load cache for " + key, e);
-        } finally {
-            if (resourceFile != null) {
-                try {
-                    resourceFile.close();
-                } catch (IOException e) {
-                    Log.d(TAG, "failed to save restored icon for: " + key, e);
+            if (c.moveToNext()) {
+                entry.icon = loadIconNoResize(c, 0);
+                entry.isLowResIcon = lowRes;
+                entry.title = c.getString(1);
+                if (entry.title == null) {
+                    entry.title = "";
+                    entry.contentDescription = "";
+                } else {
+                    entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
                 }
+                return true;
             }
+        } finally {
+            c.close();
+        }
+        return false;
+    }
+
+    public static class IconLoadRequest {
+        private final Runnable mRunnable;
+        private final Handler mHandler;
+
+        IconLoadRequest(Runnable runnable, Handler handler) {
+            mRunnable = runnable;
+            mHandler = handler;
+        }
+
+        public void cancel() {
+            mHandler.removeCallbacks(mRunnable);
         }
     }
 
-    /**
-     * Read a pre-loaded icon from the persistent icon cache.
-     *
-     * @param componentName the component that should own the icon
-     * @returns a bitmap if one is cached, or null.
-     */
-    private Bitmap getPreloadedIcon(ComponentName componentName, UserHandleCompat user) {
-        final String key = componentName.flattenToShortString();
+    private static final class IconDB extends SQLiteOpenHelper {
+        private final static int DB_VERSION = 3;
 
-        // We don't keep icons for other profiles in persistent cache.
-        if (!user.equals(UserHandleCompat.myUserHandle())) {
+        private final static String TABLE_NAME = "icons";
+        private final static String COLUMN_ROWID = "rowid";
+        private final static String COLUMN_COMPONENT = "componentName";
+        private final static String COLUMN_USER = "profileId";
+        private final static String COLUMN_LAST_UPDATED = "lastUpdated";
+        private final static String COLUMN_VERSION = "version";
+        private final static String COLUMN_ICON = "icon";
+        private final static String COLUMN_ICON_LOW_RES = "icon_low_res";
+        private final static String COLUMN_LABEL = "label";
+        private final static String COLUMN_SYSTEM_STATE = "system_state";
+
+        public String mSystemState;
+
+        public IconDB(Context context) {
+            super(context, LauncherFiles.APP_ICONS_DB, null, DB_VERSION);
+            updateSystemStateString(context);
+        }
+
+        public void updateSystemStateString(Context c) {
+            mSystemState = Locale.getDefault().toString() + ","
+                    + c.getResources().getConfiguration().mcc;
+        }
+
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
+                    COLUMN_COMPONENT + " TEXT NOT NULL, " +
+                    COLUMN_USER + " INTEGER NOT NULL, " +
+                    COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
+                    COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
+                    COLUMN_ICON + " BLOB, " +
+                    COLUMN_ICON_LOW_RES + " BLOB, " +
+                    COLUMN_LABEL + " TEXT, " +
+                    COLUMN_SYSTEM_STATE + " TEXT, " +
+                    "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") " +
+                    ");");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        private void clearDB(SQLiteDatabase db) {
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+            onCreate(db);
+        }
+
+        public ContentValues newContentValues(Bitmap icon, String label) {
+            ContentValues values = new ContentValues();
+            values.put(COLUMN_ICON, Utilities.flattenBitmap(icon));
+            values.put(COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(
+                    Bitmap.createScaledBitmap(icon,
+                            icon.getWidth() / LOW_RES_SCALE_FACTOR,
+                            icon.getHeight() / LOW_RES_SCALE_FACTOR, true)));
+            values.put(COLUMN_LABEL, label);
+            values.put(COLUMN_SYSTEM_STATE, mSystemState);
+            return values;
+        }
+    }
+
+    private static Bitmap loadIconNoResize(Cursor c, int iconIndex) {
+        byte[] data = c.getBlob(iconIndex);
+        try {
+            return BitmapFactory.decodeByteArray(data, 0, data.length);
+        } catch (Exception e) {
             return null;
         }
-
-        if (DEBUG) Log.v(TAG, "looking for pre-load icon for " + key);
-        Bitmap icon = null;
-        FileInputStream resourceFile = null;
-        try {
-            resourceFile = mContext.openFileInput(getResourceFilename(componentName));
-            byte[] buffer = new byte[1024];
-            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-            int bytesRead = 0;
-            while(bytesRead >= 0) {
-                bytes.write(buffer, 0, bytesRead);
-                bytesRead = resourceFile.read(buffer, 0, buffer.length);
-            }
-            if (DEBUG) Log.d(TAG, "read " + bytes.size());
-            icon = BitmapFactory.decodeByteArray(bytes.toByteArray(), 0, bytes.size());
-            if (icon == null) {
-                Log.w(TAG, "failed to decode pre-load icon for " + key);
-            }
-        } catch (FileNotFoundException e) {
-            if (DEBUG) Log.d(TAG, "there is no restored icon for: " + key);
-        } catch (IOException e) {
-            Log.w(TAG, "failed to read pre-load icon for: " + key, e);
-        } finally {
-            if(resourceFile != null) {
-                try {
-                    resourceFile.close();
-                } catch (IOException e) {
-                    Log.d(TAG, "failed to manage pre-load icon file: " + key, e);
-                }
-            }
-        }
-
-        return icon;
-    }
-
-    /**
-     * Remove a pre-loaded icon from the persistent icon cache.
-     *
-     * @param componentName the component that should own the icon
-     */
-    public void deletePreloadedIcon(ComponentName componentName, UserHandleCompat user) {
-        // We don't keep icons for other profiles in persistent cache.
-        if (!user.equals(UserHandleCompat.myUserHandle()) || componentName == null) {
-            return;
-        }
-        remove(componentName, user);
-        boolean success = mContext.deleteFile(getResourceFilename(componentName));
-        if (DEBUG && success) Log.d(TAG, "removed pre-loaded icon from persistent cache");
-    }
-
-    private static String getResourceFilename(ComponentName component) {
-        String resourceName = component.flattenToShortString();
-        String filename = resourceName.replace(File.separatorChar, '_');
-        return RESOURCE_FILE_PREFIX + filename;
     }
 }
diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java
index 3c36361..e48640c 100644
--- a/src/com/android/launcher3/InfoDropTarget.java
+++ b/src/com/android/launcher3/InfoDropTarget.java
@@ -18,21 +18,14 @@
 
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.drawable.TransitionDrawable;
+import android.provider.Settings;
 import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
 
+import com.android.launcher3.R;
 import com.android.launcher3.compat.UserHandleCompat;
 
 public class InfoDropTarget extends ButtonDropTarget {
 
-    private ColorStateList mOriginalTextColor;
-    private TransitionDrawable mDrawable;
-
     public InfoDropTarget(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
@@ -44,43 +37,10 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-
-        mOriginalTextColor = getTextColors();
-
         // Get the hover color
-        Resources r = getResources();
-        mHoverColor = r.getColor(R.color.info_target_hover_tint);
-        mDrawable = (TransitionDrawable) getCurrentDrawable();
+        mHoverColor = getResources().getColor(R.color.info_target_hover_tint);
 
-        if (mDrawable == null) {
-            // TODO: investigate why this is ever happening. Presently only on one known device.
-            mDrawable = (TransitionDrawable) r.getDrawable(R.drawable.info_target_selector);
-            setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null);
-        }
-
-        if (null != mDrawable) {
-            mDrawable.setCrossFadeEnabled(true);
-        }
-
-        // Remove the text in the Phone UI in landscape
-        int orientation = getResources().getConfiguration().orientation;
-        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
-            if (!LauncherAppState.getInstance().isScreenLarge()) {
-                setText("");
-            }
-        }
-    }
-
-    @Override
-    public boolean acceptDrop(DragObject d) {
-        // acceptDrop is called just before onDrop. We do the work here, rather than
-        // in onDrop, because it allows us to reject the drop (by returning false)
-        // so that the object being dragged isn't removed from the drag source.
-
-        startDetailsActivityForInfo(d.dragInfo, mLauncher);
-        // There is no post-drop animation, so clean up the DragView now
-        d.deferDragViewCleanupPostAnimation = false;
-        return false;
+        setDrawable(R.drawable.info_target_selector);
     }
 
     public static void startDetailsActivityForInfo(Object info, Launcher launcher) {
@@ -105,39 +65,14 @@
     }
 
     @Override
-    public void onDragStart(DragSource source, Object info, int dragAction) {
-        boolean isVisible = true;
-
-        // Hide this button unless we are dragging something from AllApps
-        if (!source.supportsAppInfoDropTarget()) {
-            isVisible = false;
-        }
-
-        mActive = isVisible;
-        mDrawable.resetTransition();
-        setTextColor(mOriginalTextColor);
-        ((ViewGroup) getParent()).setVisibility(isVisible ? View.VISIBLE : View.GONE);
+    protected boolean supportsDrop(DragSource source, Object info) {
+        return source.supportsAppInfoDropTarget() &&
+                Settings.Global.getInt(getContext().getContentResolver(),
+                        Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) == 1;
     }
 
     @Override
-    public void onDragEnd() {
-        super.onDragEnd();
-        mActive = false;
-    }
-
-    public void onDragEnter(DragObject d) {
-        super.onDragEnter(d);
-
-        mDrawable.startTransition(mTransitionDuration);
-        setTextColor(mHoverColor);
-    }
-
-    public void onDragExit(DragObject d) {
-        super.onDragExit(d);
-
-        if (!d.dragComplete) {
-            mDrawable.resetTransition();
-            setTextColor(mOriginalTextColor);
-        }
+    void completeDrop(DragObject d) {
+        startDetailsActivityForInfo(d.dragInfo, mLauncher);
     }
 }
diff --git a/src/com/android/launcher3/Insettable.java b/src/com/android/launcher3/Insettable.java
index 1d2356c..3b8ef2f 100644
--- a/src/com/android/launcher3/Insettable.java
+++ b/src/com/android/launcher3/Insettable.java
@@ -18,6 +18,10 @@
 
 import android.graphics.Rect;
 
+/**
+ * Allows the implementing {@link View} to not draw underneath system bars.
+ * e.g., notification bar on top and home key area on the bottom.
+ */
 public interface Insettable {
 
     void setInsets(Rect insets);
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 1ab3085..0c69154 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -22,6 +22,7 @@
 import android.content.SharedPreferences;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.text.TextUtils;
@@ -32,6 +33,7 @@
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.Thunk;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -146,6 +148,7 @@
 
         if (DBG) Log.d(TAG, "Got INSTALL_SHORTCUT: " + data.toUri(0));
         PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context);
+        info = convertToLauncherActivityIfPossible(info);
 
         queuePendingShortcutInfo(info, context);
     }
@@ -186,11 +189,6 @@
                 final PendingInstallShortcutInfo pendingInfo = iter.next();
                 final Intent intent = pendingInfo.launchIntent;
 
-                if (LauncherAppState.isDisableAllApps() && !isValidShortcutLaunchIntent(intent)) {
-                    if (DBG) Log.d(TAG, "Ignoring shortcut with launchIntent:" + intent);
-                    continue;
-                }
-
                 // If the intent specifies a package, make sure the package exists
                 String packageName = pendingInfo.getTargetPackage();
                 if (!TextUtils.isEmpty(packageName)) {
@@ -212,7 +210,7 @@
             // Add the new apps to the model and bind them
             if (!addShortcuts.isEmpty()) {
                 LauncherAppState app = LauncherAppState.getInstance();
-                app.getModel().addAndBindAddedWorkspaceApps(context, addShortcuts);
+                app.getModel().addAndBindAddedWorkspaceItems(context, addShortcuts);
             }
         }
     }
@@ -245,7 +243,7 @@
      * Ensures that we have a valid, non-null name.  If the provided name is null, we will return
      * the application name instead.
      */
-    private static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) {
+    @Thunk static CharSequence ensureValidName(Context context, Intent intent, CharSequence name) {
         if (name == null) {
             try {
                 PackageManager pm = context.getPackageManager();
@@ -335,7 +333,7 @@
                     .key(LAUNCH_INTENT_KEY).value(launchIntent.toUri(0))
                     .key(NAME_KEY).value(name);
                 if (icon != null) {
-                    byte[] iconByteArray = ItemInfo.flattenBitmap(icon);
+                    byte[] iconByteArray = Utilities.flattenBitmap(icon);
                     json = json.key(ICON_KEY).value(
                             Base64.encodeToString(
                                     iconByteArray, 0, iconByteArray.length, Base64.DEFAULT));
@@ -354,16 +352,7 @@
 
         public ShortcutInfo getShortcutInfo() {
             if (activityInfo != null) {
-                final ShortcutInfo info = new ShortcutInfo();
-                info.user = user;
-                info.title = label;
-                info.contentDescription = label;
-                info.customIcon = false;
-                info.intent = launchIntent;
-                info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-                info.flags = AppInfo.initFlags(activityInfo);
-                info.firstInstallTime = activityInfo.getFirstInstallTime();
-                return info;
+                return ShortcutInfo.fromActivityInfo(activityInfo, mContext);
             } else {
                 return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data);
             }
@@ -377,6 +366,10 @@
             }
             return packageName;
         }
+
+        public boolean isLuncherActivity() {
+            return activityInfo != null;
+        }
     }
 
     private static PendingInstallShortcutInfo decode(String encoded, Context context) {
@@ -424,4 +417,40 @@
         }
         return null;
     }
+
+    /**
+     * Tries to create a new PendingInstallShortcutInfo which represents the same target,
+     * but is an app target and not a shortcut.
+     * @return the newly created info or the original one.
+     */
+    private static PendingInstallShortcutInfo convertToLauncherActivityIfPossible(
+            PendingInstallShortcutInfo original) {
+        if (original.isLuncherActivity()) {
+            // Already an activity target
+            return original;
+        }
+        if (isValidShortcutLaunchIntent(original.launchIntent)
+                || !original.user.equals(UserHandleCompat.myUserHandle())) {
+            // We can only convert shortcuts which point to a main activity in the current user.
+            return original;
+        }
+
+        PackageManager pm = original.mContext.getPackageManager();
+        ResolveInfo info = pm.resolveActivity(original.launchIntent, 0);
+
+        if (info == null) {
+            return original;
+        }
+
+        // Ignore any conflicts in the label name, as that can change based on locale.
+        LauncherActivityInfoCompat launcherInfo = LauncherActivityInfoCompat
+                .fromResolveInfo(info, original.mContext);
+        return new PendingInstallShortcutInfo(launcherInfo, original.mContext);
+    }
+
+    public static boolean isLauncherActivity(Intent intent, Context context) {
+        Intent data = new Intent().putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent);
+        PendingInstallShortcutInfo info = new PendingInstallShortcutInfo(data, context);
+        return convertToLauncherActivityIfPossible(info).isLuncherActivity();
+    }
 }
diff --git a/src/com/android/launcher3/InterruptibleInOutAnimator.java b/src/com/android/launcher3/InterruptibleInOutAnimator.java
index 2898b34..29df38b 100644
--- a/src/com/android/launcher3/InterruptibleInOutAnimator.java
+++ b/src/com/android/launcher3/InterruptibleInOutAnimator.java
@@ -21,6 +21,8 @@
 import android.animation.ValueAnimator;
 import android.view.View;
 
+import com.android.launcher3.util.Thunk;
+
 /**
  * A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation.
  * With a regular ValueAnimator, if you call reverse to show the 'out' animation, you'll get
@@ -43,7 +45,7 @@
     private static final int OUT = 2;
 
     // TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz
-    private int mDirection = STOPPED;
+    @Thunk int mDirection = STOPPED;
 
     public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) {
         mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration);
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index aff8323..f7e0ea4 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -20,13 +20,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.util.Log;
 
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.util.Arrays;
 
 /**
@@ -39,7 +36,7 @@
      */
     static final String EXTRA_PROFILE = "profile";
     
-    static final int NO_ID = -1;
+    public static final int NO_ID = -1;
     
     /**
      * The id in the settings database for this item
@@ -85,7 +82,7 @@
     /**
      * Indicates the Y cell span.
      */
-    int spanY = 1;
+    public int spanY = 1;
 
     /**
      * Indicates the minimum X cell span.
@@ -110,21 +107,21 @@
     /**
      * Title of the item
      */
-    CharSequence title;
+    public CharSequence title;
 
     /**
      * Content description of the item.
      */
-    CharSequence contentDescription;
+    public CharSequence contentDescription;
 
     /**
      * The position of the item in a drag-and-drop operation.
      */
-    int[] dropPos = null;
+    public int[] dropPos = null;
 
-    UserHandleCompat user;
+    public UserHandleCompat user;
 
-    ItemInfo() {
+    public ItemInfo() {
         user = UserHandleCompat.myUserHandle();
     }
 
@@ -177,25 +174,9 @@
         }
     }
 
-    static byte[] flattenBitmap(Bitmap bitmap) {
-        // Try go guesstimate how much space the icon will take when serialized
-        // to avoid unnecessary allocations/copies during the write.
-        int size = bitmap.getWidth() * bitmap.getHeight() * 4;
-        ByteArrayOutputStream out = new ByteArrayOutputStream(size);
-        try {
-            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
-            out.flush();
-            out.close();
-            return out.toByteArray();
-        } catch (IOException e) {
-            Log.w("Favorite", "Could not write icon");
-            return null;
-        }
-    }
-
     static void writeBitmap(ContentValues values, Bitmap bitmap) {
         if (bitmap != null) {
-            byte[] data = flattenBitmap(bitmap);
+            byte[] data = Utilities.flattenBitmap(bitmap);
             values.put(LauncherSettings.Favorites.ICON, data);
         }
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5b8eaef..2bfb29e 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1,4 +1,3 @@
-
 /*
  * Copyright (C) 2008 The Android Open Source Project
  *
@@ -22,7 +21,6 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.PropertyValuesHolder;
-import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.Activity;
@@ -49,7 +47,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Bitmap;
@@ -83,19 +80,17 @@
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
-import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Advanceable;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import com.android.launcher3.DropTarget.DragObject;
@@ -107,6 +102,9 @@
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetsContainerView;
 
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
@@ -116,7 +114,6 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.text.DateFormat;
@@ -133,12 +130,13 @@
  */
 public class Launcher extends Activity
         implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,
-                   View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener {
+                   View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener,
+                   LauncherStateTransitionAnimation.Callbacks {
     static final String TAG = "Launcher";
-    static final boolean LOGD = false;
+    static final boolean LOGD = true;
 
     static final boolean PROFILE_STARTUP = false;
-    static final boolean DEBUG_WIDGETS = false;
+    static final boolean DEBUG_WIDGETS = true;
     static final boolean DEBUG_STRICT_MODE = false;
     static final boolean DEBUG_RESUME_TIME = false;
     static final boolean DEBUG_DUMP_LOG = false;
@@ -147,7 +145,6 @@
 
     private static final int REQUEST_CREATE_SHORTCUT = 1;
     private static final int REQUEST_CREATE_APPWIDGET = 5;
-    private static final int REQUEST_PICK_SHORTCUT = 7;
     private static final int REQUEST_PICK_APPWIDGET = 9;
     private static final int REQUEST_PICK_WALLPAPER = 10;
 
@@ -167,7 +164,6 @@
     // To turn on these properties, type
     // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS]
     static final String DUMP_STATE_PROPERTY = "launcher_dump_state";
-    static final String DISABLE_ALL_APPS_PROPERTY = "launcher_noallapps";
 
     // The Intent extra that defines whether to ignore the launch animation
     static final String INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION =
@@ -216,9 +212,10 @@
     public static final String USER_HAS_MIGRATED = "launcher.user_migrated_from_old_data";
 
     /** The different states that Launcher can be in. */
-    private enum State { NONE, WORKSPACE, APPS_CUSTOMIZE, APPS_CUSTOMIZE_SPRING_LOADED };
-    private State mState = State.WORKSPACE;
-    private AnimatorSet mStateAnimation;
+    enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED };
+    @Thunk State mState = State.WORKSPACE;
+    @Thunk AnimatorSet mStateAnimation;
+    @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation;
 
     private boolean mIsSafeModeEnabled;
 
@@ -231,16 +228,13 @@
     private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
     private static final int ACTIVITY_START_DELAY = 1000;
 
-    private static final Object sLock = new Object();
-
     private HashMap<Integer, Integer> mItemIdToViewId = new HashMap<Integer, Integer>();
     private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
 
     // How long to wait before the new-shortcut animation automatically pans the workspace
     private static int NEW_APPS_PAGE_MOVE_DELAY = 500;
     private static int NEW_APPS_ANIMATION_INACTIVE_TIMEOUT_SECONDS = 5;
-    private static int NEW_APPS_ANIMATION_DELAY = 500;
-    private static final int SINGLE_FRAME_DELAY = 16;
+    @Thunk static int NEW_APPS_ANIMATION_DELAY = 500;
 
     private final BroadcastReceiver mCloseSystemDialogsReceiver
             = new CloseSystemDialogsIntentReceiver();
@@ -248,17 +242,17 @@
 
     private LayoutInflater mInflater;
 
-    private Workspace mWorkspace;
+    @Thunk Workspace mWorkspace;
     private View mLauncherView;
     private View mPageIndicators;
-    private DragLayer mDragLayer;
+    @Thunk DragLayer mDragLayer;
     private DragController mDragController;
     private View mWeightWatcher;
 
     private AppWidgetManagerCompat mAppWidgetManager;
     private LauncherAppWidgetHost mAppWidgetHost;
 
-    private ItemInfo mPendingAddInfo = new ItemInfo();
+    @Thunk ItemInfo mPendingAddInfo = new ItemInfo();
     private LauncherAppWidgetProviderInfo mPendingAddWidgetInfo;
     private int mPendingAddWidgetId = -1;
 
@@ -272,8 +266,13 @@
     private View mAllAppsButton;
 
     private SearchDropTargetBar mSearchDropTargetBar;
-    private AppsCustomizeTabHost mAppsCustomizeTabHost;
-    private AppsCustomizePagedView mAppsCustomizeContent;
+
+    // Main container view for the all apps screen.
+    @Thunk AppsContainerView mAppsView;
+
+    // Main container view for the widget tray screen.
+    private WidgetsContainerView mWidgetsView;
+
     private boolean mAutoAdvanceRunning = false;
     private AppWidgetHostView mQsb;
 
@@ -285,7 +284,7 @@
 
     private SpannableStringBuilder mDefaultKeySsb = null;
 
-    private boolean mWorkspaceLoading = true;
+    @Thunk boolean mWorkspaceLoading = true;
 
     private boolean mPaused = true;
     private boolean mRestoring;
@@ -299,34 +298,31 @@
 
     private LauncherModel mModel;
     private IconCache mIconCache;
-    private boolean mUserPresent = true;
+    @Thunk boolean mUserPresent = true;
     private boolean mVisible = false;
     private boolean mHasFocus = false;
     private boolean mAttached = false;
 
-    private static LocaleConfiguration sLocaleConfiguration = null;
+    @Thunk static LocaleConfiguration sLocaleConfiguration = null;
 
     private static HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>();
 
     private View.OnTouchListener mHapticFeedbackTouchListener;
 
-    public static final int BUILD_LAYER = 0;
-    public static final int BUILD_AND_SET_LAYER = 1;
-
     // Related to the auto-advancing of widgets
     private final int ADVANCE_MSG = 1;
     private final int mAdvanceInterval = 20000;
     private final int mAdvanceStagger = 250;
     private long mAutoAdvanceSentTime;
     private long mAutoAdvanceTimeLeft = -1;
-    private HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance =
+    @Thunk HashMap<View, AppWidgetProviderInfo> mWidgetsToAdvance =
         new HashMap<View, AppWidgetProviderInfo>();
 
     // Determines how long to wait after a rotation before restoring the screen orientation to
     // match the sensor state.
     private final int mRestoreScreenOrientationDelay = 500;
 
-    private Drawable mWorkspaceBackgroundDrawable;
+    @Thunk Drawable mWorkspaceBackgroundDrawable;
 
     private final ArrayList<Integer> mSynchronouslyBoundPages = new ArrayList<Integer>();
     private static final boolean DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE = false;
@@ -342,11 +338,9 @@
     // it from the context.
     private SharedPreferences mSharedPrefs;
 
-    private static ArrayList<ComponentName> mIntentsOnWorkspaceFromUpgradePath = null;
-
     // Holds the page that we need to animate to, and the icon views that we need to animate up
     // when we scroll to that page on resume.
-    private ImageView mFolderIconImageView;
+    @Thunk ImageView mFolderIconImageView;
     private Bitmap mFolderIconBitmap;
     private Canvas mFolderIconCanvas;
     private Rect mRectForFolderAnimation = new Rect();
@@ -363,7 +357,19 @@
         }
     }
 
-    private Runnable mBuildLayersRunnable = new Runnable() {
+    // TODO: remove this field and call method directly when Launcher3 can depend on M APIs
+    private static Method sClipRevealMethod = null;
+    static {
+        Class<?> activityOptionsClass = ActivityOptions.class;
+        try {
+            sClipRevealMethod = activityOptionsClass.getDeclaredMethod("makeClipRevealAnimation",
+                    View.class, int.class, int.class, int.class, int.class);
+        } catch (Exception e) {
+            // Earlier version
+        }
+    }
+
+    @Thunk Runnable mBuildLayersRunnable = new Runnable() {
         public void run() {
             if (mWorkspace != null) {
                 mWorkspace.buildPageHardwareLayers();
@@ -373,7 +379,7 @@
 
     private static PendingAddArguments sPendingAddItem;
 
-    private static class PendingAddArguments {
+    @Thunk static class PendingAddArguments {
         int requestCode;
         Intent intent;
         long container;
@@ -426,6 +432,7 @@
         mIconCache.flushInvalidIcons(grid);
         mDragController = new DragController(this);
         mInflater = getLayoutInflater();
+        mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, this);
 
         mStats = new Stats(this);
 
@@ -513,6 +520,17 @@
 
     public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
         mLauncherCallbacks = callbacks;
+        mLauncherCallbacks.setLauncherAppsCallback(new Launcher.LauncherAppsCallbacks() {
+            @Override
+            public void onAllAppsBoundsChanged(Rect bounds) {
+                mAppsView.setFixedBounds(Launcher.this, bounds);
+            }
+
+            @Override
+            public void dismissAllApps() {
+                showWorkspace(true);
+            }
+        });
         return true;
     }
 
@@ -561,7 +579,7 @@
         }
     }
 
-    private void checkForLocaleChange() {
+    @Thunk void checkForLocaleChange() {
         if (sLocaleConfiguration == null) {
             new AsyncTask<Void, Void, LocaleConfiguration>() {
                 @Override
@@ -610,13 +628,13 @@
         }
     }
 
-    private static class LocaleConfiguration {
+    @Thunk static class LocaleConfiguration {
         public String locale;
         public int mcc = -1;
         public int mnc = -1;
     }
 
-    private static void readConfiguration(Context context, LocaleConfiguration configuration) {
+    @Thunk static void readConfiguration(Context context, LocaleConfiguration configuration) {
         DataInputStream in = null;
         try {
             in = new DataInputStream(context.openFileInput(LauncherFiles.LAUNCHER_PREFERENCES));
@@ -638,7 +656,7 @@
         }
     }
 
-    private static void writeConfiguration(Context context, LocaleConfiguration configuration) {
+    @Thunk static void writeConfiguration(Context context, LocaleConfiguration configuration) {
         DataOutputStream out = null;
         try {
             out = new DataOutputStream(context.openFileOutput(
@@ -671,7 +689,7 @@
         return mInflater;
     }
 
-    boolean isDraggingEnabled() {
+    public boolean isDraggingEnabled() {
         // We prevent dragging when we are loading the workspace as it is possible to pick up a view
         // that is subsequently removed from the workspace in startBinding().
         return !mModel.isLoadingWorkspace();
@@ -915,7 +933,7 @@
         }
     }
 
-    private void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) {
+    @Thunk void completeTwoStageWidgetDrop(final int resultCode, final int appWidgetId) {
         CellLayout cellLayout =
                 (CellLayout) mWorkspace.getScreenWithId(mPendingAddInfo.screenId);
         Runnable onCompleteRunnable = null;
@@ -987,8 +1005,10 @@
         // Restore the previous launcher state
         if (mOnResumeState == State.WORKSPACE) {
             showWorkspace(false);
-        } else if (mOnResumeState == State.APPS_CUSTOMIZE) {
-            showAllApps(false, mAppsCustomizeContent.getContentType(), false);
+        } else if (mOnResumeState == State.APPS) {
+            showAppsView(false /* animated */, false /* resetListToTop */);
+        } else if (mOnResumeState == State.WIDGETS) {
+            showWidgetsView(false, false);
         }
         mOnResumeState = State.NONE;
 
@@ -1010,15 +1030,9 @@
                 startTimeCallbacks = System.currentTimeMillis();
             }
 
-            if (mAppsCustomizeContent != null) {
-                mAppsCustomizeContent.setBulkBind(true);
-            }
             for (int i = 0; i < mBindOnResumeCallbacks.size(); i++) {
                 mBindOnResumeCallbacks.get(i).run();
             }
-            if (mAppsCustomizeContent != null) {
-                mAppsCustomizeContent.setBulkBind(false);
-            }
             mBindOnResumeCallbacks.clear();
             if (DEBUG_RESUME_TIME) {
                 Log.d(TAG, "Time spent processing callbacks in onResume: " +
@@ -1044,6 +1058,7 @@
         // (framework issue). On resuming, we ensure that any widgets are inflated for the current
         // orientation.
         getWorkspace().reinflateWidgetsIfNecessary();
+        reinflateQSBIfNecessary();
 
         // Process any items that were added while Launcher was away.
         InstallShortcutReceiver.disableAndFlushInstallQueue(this);
@@ -1137,6 +1152,19 @@
         public void forceExitFullImmersion();
     }
 
+    public interface LauncherAppsCallbacks {
+        /**
+         * Updates launcher to the available space that AllApps can take so as not to overlap with
+         * any other views.
+         */
+        public void onAllAppsBoundsChanged(Rect bounds);
+
+        /**
+         * Called to dismiss all apps if it is showing.
+         */
+        public void dismissAllApps();
+    }
+
     public interface LauncherOverlayCallbacks {
         /**
          * This method indicates whether a call to {@link #enterFullImmersion()} will succeed,
@@ -1209,9 +1237,8 @@
         if (mModel.isCurrentCallbacks(this)) {
             mModel.stopLoader();
         }
-        if (mAppsCustomizeContent != null) {
-            mAppsCustomizeContent.surrender();
-        }
+        //TODO(hyunyoungs): stop the widgets loader when there is a rotation.
+
         return Boolean.TRUE;
     }
 
@@ -1297,8 +1324,8 @@
         }
 
         State state = intToState(savedState.getInt(RUNTIME_STATE, State.WORKSPACE.ordinal()));
-        if (state == State.APPS_CUSTOMIZE) {
-            mOnResumeState = State.APPS_CUSTOMIZE;
+        if (state == State.APPS || state == State.WIDGETS) {
+            mOnResumeState = state;
         }
 
         int currentScreen = savedState.getInt(RUNTIME_STATE_CURRENT_SCREEN,
@@ -1332,19 +1359,6 @@
             mRestoring = true;
         }
 
-        // Restore the AppsCustomize tab
-        if (mAppsCustomizeTabHost != null) {
-            String curTab = savedState.getString("apps_customize_currentTab");
-            if (curTab != null) {
-                mAppsCustomizeTabHost.setContentTypeImmediate(
-                        mAppsCustomizeTabHost.getContentTypeForTabTag(curTab));
-                mAppsCustomizeContent.loadAssociatedPages(
-                        mAppsCustomizeContent.getCurrentPage());
-            }
-
-            int currentIndex = savedState.getInt("apps_customize_currentIndex");
-            mAppsCustomizeContent.restorePageForIndex(currentIndex);
-        }
         mItemIdToViewId = (HashMap<Integer, Integer>)
                 savedState.getSerializable(RUNTIME_STATE_VIEW_IDS);
     }
@@ -1426,11 +1440,14 @@
         mSearchDropTargetBar = (SearchDropTargetBar)
                 mDragLayer.findViewById(R.id.search_drop_target_bar);
 
+        // Setup Apps
+        mAppsView = (AppsContainerView) findViewById(R.id.apps_view);
+        if (mLauncherCallbacks != null && mLauncherCallbacks.overrideAllAppsSearch()) {
+            mAppsView.hideSearchBar();
+        }
+
         // Setup AppsCustomize
-        mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane);
-        mAppsCustomizeContent = (AppsCustomizePagedView)
-                mAppsCustomizeTabHost.findViewById(R.id.apps_customize_pane_content);
-        mAppsCustomizeContent.setup(this, dragController);
+        mWidgetsView = (WidgetsContainerView) findViewById(R.id.widgets_view);
 
         // Setup the drag controller (drop targets have to be added in reverse order in priority)
         dragController.setDragScoller(mWorkspace);
@@ -1439,7 +1456,7 @@
         dragController.addDropTarget(mWorkspace);
         if (mSearchDropTargetBar != null) {
             mSearchDropTargetBar.setup(this, dragController);
-            mSearchDropTargetBar.setQsbSearchBar(getQsbBar());
+            mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar());
         }
 
         if (getResources().getBoolean(R.bool.debug_memory_enabled)) {
@@ -1548,7 +1565,7 @@
             return;
         }
 
-        LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1], false);
+        LauncherModel.addItemToDatabase(this, info, container, screenId, cellXY[0], cellXY[1]);
 
         if (!mRestoring) {
             mWorkspace.addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1,
@@ -1588,7 +1605,7 @@
      *
      * @param appWidgetId The app widget id
      */
-    private void completeAddAppWidget(int appWidgetId, long container, long screenId,
+    @Thunk void completeAddAppWidget(int appWidgetId, long container, long screenId,
             AppWidgetHostView hostView, LauncherAppWidgetProviderInfo appWidgetInfo) {
 
         ItemInfo info = mPendingAddInfo;
@@ -1610,7 +1627,7 @@
         launcherInfo.user = mAppWidgetManager.getUser(appWidgetInfo);
 
         LauncherModel.addItemToDatabase(this, launcherInfo,
-                container, screenId, info.cellX, info.cellY, false);
+                container, screenId, info.cellX, info.cellY);
 
         if (!mRestoring) {
             if (hostView == null) {
@@ -1640,16 +1657,17 @@
             if (Intent.ACTION_SCREEN_OFF.equals(action)) {
                 mUserPresent = false;
                 mDragLayer.clearAllResizeFrames();
-                updateRunning();
+                updateAutoAdvanceState();
 
                 // Reset AllApps to its initial state only if we are not in the middle of
                 // processing a multi-step drop
-                if (mAppsCustomizeTabHost != null && mPendingAddInfo.container == ItemInfo.NO_ID) {
+                if (mAppsView != null && mWidgetsView != null &&
+                        mPendingAddInfo.container == ItemInfo.NO_ID) {
                     showWorkspace(false);
                 }
             } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
                 mUserPresent = true;
-                updateRunning();
+                updateAutoAdvanceState();
             } else if (ENABLE_DEBUG_INTENTS && DebugIntents.DELETE_DATABASE.equals(action)) {
                 mModel.resetLoadedState(false, true);
                 mModel.startLoader(false, PagedView.INVALID_RESTORE_PAGE,
@@ -1692,40 +1710,19 @@
      * Sets up transparent navigation and status bars in LMP.
      * This method is a no-op for other platform versions.
      */
-    @TargetApi(19)
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     private void setupTransparentSystemBarsForLmp() {
-        // TODO(sansid): use the APIs directly when compiling against L sdk.
-        // Currently we use reflection to access the flags and the API to set the transparency
-        // on the System bars.
         if (Utilities.isLmpOrAbove()) {
-            try {
-                getWindow().getAttributes().systemUiVisibility |=
-                        (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
-                                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
-                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
-                        | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
-                Field drawsSysBackgroundsField = WindowManager.LayoutParams.class.getField(
-                        "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS");
-                getWindow().addFlags(drawsSysBackgroundsField.getInt(null));
-
-                Method setStatusBarColorMethod =
-                        Window.class.getDeclaredMethod("setStatusBarColor", int.class);
-                Method setNavigationBarColorMethod =
-                        Window.class.getDeclaredMethod("setNavigationBarColor", int.class);
-                setStatusBarColorMethod.invoke(getWindow(), Color.TRANSPARENT);
-                setNavigationBarColorMethod.invoke(getWindow(), Color.TRANSPARENT);
-            } catch (NoSuchFieldException e) {
-                Log.w(TAG, "NoSuchFieldException while setting up transparent bars");
-            } catch (NoSuchMethodException ex) {
-                Log.w(TAG, "NoSuchMethodException while setting up transparent bars");
-            } catch (IllegalAccessException e) {
-                Log.w(TAG, "IllegalAccessException while setting up transparent bars");
-            } catch (IllegalArgumentException e) {
-                Log.w(TAG, "IllegalArgumentException while setting up transparent bars");
-            } catch (InvocationTargetException e) {
-                Log.w(TAG, "InvocationTargetException while setting up transparent bars");
-            } finally {}
+            Window window = getWindow();
+            window.getAttributes().systemUiVisibility |=
+                    (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                            | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                            | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+                    | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            window.setStatusBarColor(Color.TRANSPARENT);
+            window.setNavigationBarColor(Color.TRANSPARENT);
         }
     }
 
@@ -1738,17 +1735,16 @@
             unregisterReceiver(mReceiver);
             mAttached = false;
         }
-        updateRunning();
+        updateAutoAdvanceState();
     }
 
     public void onWindowVisibilityChanged(int visibility) {
         mVisible = visibility == View.VISIBLE;
-        updateRunning();
+        updateAutoAdvanceState();
         // The following code used to be in onResume, but it turns out onResume is called when
         // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged
         // is a more appropriate event to handle
         if (mVisible) {
-            mAppsCustomizeTabHost.onWindowVisible();
             if (!mWorkspaceLoading) {
                 final ViewTreeObserver observer = mWorkspace.getViewTreeObserver();
                 // We want to let Launcher draw itself at least once before we force it to build
@@ -1783,14 +1779,14 @@
         }
     }
 
-    private void sendAdvanceMessage(long delay) {
+    @Thunk void sendAdvanceMessage(long delay) {
         mHandler.removeMessages(ADVANCE_MSG);
         Message msg = mHandler.obtainMessage(ADVANCE_MSG);
         mHandler.sendMessageDelayed(msg, delay);
         mAutoAdvanceSentTime = System.currentTimeMillis();
     }
 
-    private void updateRunning() {
+    @Thunk void updateAutoAdvanceState() {
         boolean autoAdvanceRunning = mVisible && mUserPresent && !mWidgetsToAdvance.isEmpty();
         if (autoAdvanceRunning != mAutoAdvanceRunning) {
             mAutoAdvanceRunning = autoAdvanceRunning;
@@ -1836,14 +1832,14 @@
         if (v instanceof Advanceable) {
             mWidgetsToAdvance.put(hostView, appWidgetInfo);
             ((Advanceable) v).fyiWillBeAdvancedByHostKThx();
-            updateRunning();
+            updateAutoAdvanceState();
         }
     }
 
     void removeWidgetToAutoAdvance(View hostView) {
         if (mWidgetsToAdvance.containsKey(hostView)) {
             mWidgetsToAdvance.remove(hostView);
-            updateRunning();
+            updateAutoAdvanceState();
         }
     }
 
@@ -1852,19 +1848,23 @@
         launcherInfo.hostView = null;
     }
 
-    void showOutOfSpaceMessage(boolean isHotseatLayout) {
+    public void showOutOfSpaceMessage(boolean isHotseatLayout) {
         int strId = (isHotseatLayout ? R.string.hotseat_out_of_space : R.string.out_of_space);
         Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT).show();
     }
 
-    public ArrayList<AppInfo> getAllAppsList() {
-        return mAppsCustomizeContent.getApps();
-    }
-
     public DragLayer getDragLayer() {
         return mDragLayer;
     }
 
+    public AppsContainerView getAppsView() {
+        return mAppsView;
+    }
+
+    public WidgetsContainerView getWidgetsView() {
+        return mWidgetsView;
+    }
+
     public Workspace getWorkspace() {
         return mWorkspace;
     }
@@ -1950,9 +1950,14 @@
                 imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
             }
 
-            // Reset the apps customize page
-            if (!alreadyOnHome && mAppsCustomizeTabHost != null) {
-                mAppsCustomizeTabHost.reset();
+            // Reset the apps view
+            if (!alreadyOnHome && mAppsView != null) {
+                mAppsView.scrollToTop();
+            }
+
+            // Reset the widgets view
+            if (!alreadyOnHome && mWidgetsView != null) {
+                mWidgetsView.scrollToTop();
             }
 
             if (mLauncherCallbacks != null) {
@@ -2007,16 +2012,8 @@
             outState.putLong(RUNTIME_STATE_PENDING_FOLDER_RENAME_ID, mFolderInfo.id);
         }
 
-        // Save the current AppsCustomize tab
-        if (mAppsCustomizeTabHost != null) {
-            AppsCustomizePagedView.ContentType type = mAppsCustomizeContent.getContentType();
-            String currentTabTag = mAppsCustomizeTabHost.getTabTagForContentType(type);
-            if (currentTabTag != null) {
-                outState.putString("apps_customize_currentTab", currentTabTag);
-            }
-            int currentIndex = mAppsCustomizeContent.getSaveInstanceStateIndex();
-            outState.putInt("apps_customize_currentIndex", currentIndex);
-        }
+        // Save the current widgets tray?
+        // TODO(hyunyoungs)
         outState.putSerializable(RUNTIME_STATE_VIEW_IDS, mItemIdToViewId);
 
         if (mLauncherCallbacks != null) {
@@ -2414,8 +2411,8 @@
         folderInfo.title = getText(R.string.folder_name);
 
         // Update the model
-        LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId, cellX, cellY,
-                false);
+        LauncherModel.addItemToDatabase(Launcher.this, folderInfo, container, screenId,
+                cellX, cellY);
         sFolders.put(folderInfo.id, folderInfo);
 
         // Create the view
@@ -2433,13 +2430,6 @@
         sFolders.remove(folder.id);
     }
 
-    protected ComponentName getWallpaperPickerComponent() {
-        if (mLauncherCallbacks != null) {
-            return mLauncherCallbacks.getWallpaperPickerComponent();
-        }
-        return new ComponentName(getPackageName(), LauncherWallpaperPickerActivity.class.getName());
-    }
-
     /**
      * Registers various content observers. The current implementation registers
      * only a favorites observer to keep track of the favorites applications.
@@ -2479,13 +2469,16 @@
             return;
         }
 
-        if (isAllAppsVisible()) {
-            if (mAppsCustomizeContent.getContentType() ==
-                    AppsCustomizePagedView.ContentType.Applications) {
-                showWorkspace(true);
-            } else {
-                showOverviewMode(true);
-            }
+        LauncherAccessibilityDelegate delegate =
+                LauncherAppState.getInstance().getAccessibilityDelegate();
+        if (delegate != null && delegate.onBackPressed()) {
+            return;
+        }
+
+        if (isAppsViewVisible()) {
+            showWorkspace(true);
+        } else if (isWidgetsViewVisible())  {
+            showOverviewMode(true);
         } else if (mWorkspace.isInOverviewMode()) {
             mWorkspace.exitOverviewMode(true);
         } else if (mWorkspace.getOpenFolder() != null) {
@@ -2506,7 +2499,7 @@
     /**
      * Re-listen when widgets are reset.
      */
-    private void onAppWidgetReset() {
+    @Thunk void onAppWidgetReset() {
         if (mAppWidgetHost != null) {
             mAppWidgetHost.startListening();
         }
@@ -2616,13 +2609,10 @@
      */
     protected void onClickAllAppsButton(View v) {
         if (LOGD) Log.d(TAG, "onClickAllAppsButton");
-        if (isAllAppsVisible()) {
+        if (isAppsViewVisible()) {
             showWorkspace(true);
         } else {
-            showAllApps(true, AppsCustomizePagedView.ContentType.Applications, false);
-        }
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onClickAllAppsButton(v);
+            showAppsView(true /* animated */, false /* resetListToTop */);
         }
     }
 
@@ -2704,7 +2694,7 @@
         }
     }
 
-    private void startAppShortcutOrInfoActivity(View v) {
+    @Thunk void startAppShortcutOrInfoActivity(View v) {
         Object tag = v.getTag();
         final ShortcutInfo shortcut;
         final Intent intent;
@@ -2790,7 +2780,7 @@
         if (mIsSafeModeEnabled) {
             Toast.makeText(this, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
         } else {
-            showAllApps(true, AppsCustomizePagedView.ContentType.Widgets, true);
+            showWidgetsView(true /* animated */, true /* resetPageToZero */);
             if (mLauncherCallbacks != null) {
                 mLauncherCallbacks.onClickAddWidgetButton(view);
             }
@@ -2803,9 +2793,8 @@
      */
     protected void onClickWallpaperPicker(View v) {
         if (LOGD) Log.d(TAG, "onClickWallpaperPicker");
-        final Intent pickWallpaper = new Intent(Intent.ACTION_SET_WALLPAPER);
-        pickWallpaper.setComponent(getWallpaperPickerComponent());
-        startActivityForResult(pickWallpaper, REQUEST_PICK_WALLPAPER);
+        startActivityForResult(new Intent(Intent.ACTION_SET_WALLPAPER).setPackage(getPackageName()),
+                REQUEST_PICK_WALLPAPER);
 
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onClickWallpaperPicker(v);
@@ -2939,12 +2928,39 @@
             }
 
             Bundle optsBundle = null;
-            if (useLaunchAnimation && !Utilities.isLmpOrAbove()) {
-                // On pre-L devices, we use the scale up transition.
-                // Otherwise we use system default.
-                ActivityOptions opts =
-                        ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
-                optsBundle = opts.toBundle();
+            if (useLaunchAnimation) {
+                ActivityOptions opts = null;
+                if (sClipRevealMethod != null) {
+                    // TODO: call method directly when Launcher3 can depend on M APIs
+                    int left = 0, top = 0;
+                    int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
+                    if (v instanceof TextView) {
+                        // Launch from center of icon, not entire view
+                        Drawable icon = Workspace.getTextViewIcon((TextView) v);
+                        if (icon != null) {
+                            Rect bounds = icon.getBounds();
+                            left = (width - bounds.width()) / 2;
+                            top = v.getPaddingTop();
+                            width = bounds.width();
+                            height = bounds.height();
+                        }
+                    }
+                    try {
+                        opts = (ActivityOptions) sClipRevealMethod.invoke(null, v,
+                                left, top, width, height);
+                    } catch (IllegalAccessException e) {
+                        Log.d(TAG, "Could not call makeClipRevealAnimation: " + e);
+                        sClipRevealMethod = null;
+                    } catch (InvocationTargetException e) {
+                        Log.d(TAG, "Could not call makeClipRevealAnimation: " + e);
+                        sClipRevealMethod = null;
+                    }
+                }
+                if (opts == null && !Utilities.isLmpOrAbove()) {
+                    opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0,
+                                    v.getMeasuredWidth(), v.getMeasuredHeight());
+                }
+                optsBundle = opts != null ? opts.toBundle() : null;
             }
 
             if (user == null || user.equals(UserHandleCompat.myUserHandle())) {
@@ -3096,10 +3112,19 @@
      */
     public void openFolder(FolderIcon folderIcon) {
         Folder folder = folderIcon.getFolder();
+        Folder openFolder = mWorkspace != null ? mWorkspace.getOpenFolder() : null;
+        if (openFolder != null && openFolder != folder) {
+            // Close any open folder before opening a folder.
+            closeFolder();
+        }
+
         FolderInfo info = folder.mInfo;
 
         info.opened = true;
 
+        // While the folder is open, the position of the icon cannot change.
+        ((CellLayout.LayoutParams) folderIcon.getLayoutParams()).canReorder = false;
+
         // Just verify that the folder hasn't already been added to the DragLayer.
         // There was a one-off crash where the folder had a parent already.
         if (folder.getParent() == null) {
@@ -3135,6 +3160,9 @@
         if (parent != null) {
             FolderIcon fi = (FolderIcon) mWorkspace.getViewForTag(folder.mInfo);
             shrinkAndFadeInFolderIcon(fi);
+            if (fi != null) {
+                ((CellLayout.LayoutParams) fi.getLayoutParams()).canReorder = true;
+            }
         }
         folder.animateClosed();
 
@@ -3166,7 +3194,7 @@
         View itemUnderLongClick = null;
         if (v.getTag() instanceof ItemInfo) {
             ItemInfo info = (ItemInfo) v.getTag();
-            longClickCellInfo = new CellLayout.CellInfo(v, info);;
+            longClickCellInfo = new CellLayout.CellInfo(v, info);
             itemUnderLongClick = longClickCellInfo.cell;
             resetAddInfo();
         }
@@ -3207,7 +3235,7 @@
     /**
      * Returns the CellLayout of the specified container at the specified screen.
      */
-    CellLayout getCellLayout(long container, long screenId) {
+    public CellLayout getCellLayout(long container, long screenId) {
         if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
             if (mHotseat != null) {
                 return mHotseat.getLayout();
@@ -3215,12 +3243,23 @@
                 return null;
             }
         } else {
-            return (CellLayout) mWorkspace.getScreenWithId(screenId);
+            return mWorkspace.getScreenWithId(screenId);
         }
     }
 
+    /**
+     * For overridden classes.
+     */
     public boolean isAllAppsVisible() {
-        return (mState == State.APPS_CUSTOMIZE) || (mOnResumeState == State.APPS_CUSTOMIZE);
+        return isAppsViewVisible();
+    }
+
+    public boolean isAppsViewVisible() {
+        return (mState == State.APPS) || (mOnResumeState == State.APPS);
+    }
+
+    public boolean isWidgetsViewVisible() {
+        return (mState == State.WIDGETS) || (mOnResumeState == State.WIDGETS);
     }
 
     private void setWorkspaceBackground(boolean workspace) {
@@ -3238,579 +3277,6 @@
         setWorkspaceBackground(visible);
     }
 
-    private void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) {
-        if (v instanceof LauncherTransitionable) {
-            ((LauncherTransitionable) v).onLauncherTransitionPrepare(this, animated, toWorkspace);
-        }
-    }
-
-    private void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) {
-        if (v instanceof LauncherTransitionable) {
-            ((LauncherTransitionable) v).onLauncherTransitionStart(this, animated, toWorkspace);
-        }
-
-        // Update the workspace transition step as well
-        dispatchOnLauncherTransitionStep(v, 0f);
-    }
-
-    private void dispatchOnLauncherTransitionStep(View v, float t) {
-        if (v instanceof LauncherTransitionable) {
-            ((LauncherTransitionable) v).onLauncherTransitionStep(this, t);
-        }
-    }
-
-    private void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) {
-        if (v instanceof LauncherTransitionable) {
-            ((LauncherTransitionable) v).onLauncherTransitionEnd(this, animated, toWorkspace);
-        }
-
-        // Update the workspace transition step as well
-        dispatchOnLauncherTransitionStep(v, 1f);
-    }
-
-    /**
-     * Things to test when changing the following seven functions.
-     *   - Home from workspace
-     *          - from center screen
-     *          - from other screens
-     *   - Home from all apps
-     *          - from center screen
-     *          - from other screens
-     *   - Back from all apps
-     *          - from center screen
-     *          - from other screens
-     *   - Launch app from workspace and quit
-     *          - with back
-     *          - with home
-     *   - Launch app from all apps and quit
-     *          - with back
-     *          - with home
-     *   - Go to a screen that's not the default, then all
-     *     apps, and launch and app, and go back
-     *          - with back
-     *          -with home
-     *   - On workspace, long press power and go back
-     *          - with back
-     *          - with home
-     *   - On all apps, long press power and go back
-     *          - with back
-     *          - with home
-     *   - On workspace, power off
-     *   - On all apps, power off
-     *   - Launch an app and turn off the screen while in that app
-     *          - Go back with home key
-     *          - Go back with back key  TODO: make this not go to workspace
-     *          - From all apps
-     *          - From workspace
-     *   - Enter and exit car mode (becuase it causes an extra configuration changed)
-     *          - From all apps
-     *          - From the center workspace
-     *          - From another workspace
-     */
-
-    /**
-     * Zoom the camera out from the workspace to reveal 'toView'.
-     * Assumes that the view to show is anchored at either the very top or very bottom
-     * of the screen.
-     */
-    private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) {
-        AppsCustomizePagedView.ContentType contentType = mAppsCustomizeContent.getContentType();
-        showAppsCustomizeHelper(animated, springLoaded, contentType);
-    }
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded,
-                                         final AppsCustomizePagedView.ContentType contentType) {
-        if (mStateAnimation != null) {
-            mStateAnimation.setDuration(0);
-            mStateAnimation.cancel();
-            mStateAnimation = null;
-        }
-
-        boolean material = Utilities.isLmpOrAbove();
-
-        final Resources res = getResources();
-
-        final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime);
-        final int itemsAlphaStagger =
-                res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger);
-
-        final View fromView = mWorkspace;
-        final AppsCustomizeTabHost toView = mAppsCustomizeTabHost;
-
-        final HashMap<View, Integer> layerViews = new HashMap<View, Integer>();
-
-        Workspace.State workspaceState = contentType == AppsCustomizePagedView.ContentType.Widgets ?
-                Workspace.State.OVERVIEW_HIDDEN : Workspace.State.NORMAL_HIDDEN;
-        Animator workspaceAnim =
-                mWorkspace.getChangeStateAnimation(workspaceState, animated, layerViews);
-        if (!LauncherAppState.isDisableAllApps()
-                || contentType == AppsCustomizePagedView.ContentType.Widgets) {
-            // Set the content type for the all apps/widgets space
-            mAppsCustomizeTabHost.setContentTypeImmediate(contentType);
-        }
-
-        // If for some reason our views aren't initialized, don't animate
-        boolean initialized = getAllAppsButton() != null;
-
-        if (animated && initialized) {
-            mStateAnimation = LauncherAnimUtils.createAnimatorSet();
-            final AppsCustomizePagedView content = (AppsCustomizePagedView)
-                    toView.findViewById(R.id.apps_customize_pane_content);
-
-            final View page = content.getPageAt(content.getCurrentPage());
-            final View revealView = toView.findViewById(R.id.fake_page);
-
-            final boolean isWidgetTray = contentType == AppsCustomizePagedView.ContentType.Widgets;
-            if (isWidgetTray) {
-                revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark));
-            } else {
-                revealView.setBackground(res.getDrawable(R.drawable.quantum_panel));
-            }
-
-            // Hide the real page background, and swap in the fake one
-            content.setPageBackgroundsVisible(false);
-            revealView.setVisibility(View.VISIBLE);
-            // We need to hide this view as the animation start will be posted.
-            revealView.setAlpha(0);
-
-            int width = revealView.getMeasuredWidth();
-            int height = revealView.getMeasuredHeight();
-            float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4);
-
-            revealView.setTranslationY(0);
-            revealView.setTranslationX(0);
-
-            // Get the y delta between the center of the page and the center of the all apps button
-            int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
-                    getAllAppsButton(), null);
-
-            float alpha = 0;
-            float xDrift = 0;
-            float yDrift = 0;
-            if (material) {
-                alpha = isWidgetTray ? 0.3f : 1f;
-                yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1];
-                xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0];
-            } else {
-                yDrift = 2 * height / 3;
-                xDrift = 0;
-            }
-            final float initAlpha = alpha;
-
-            layerViews.put(revealView, BUILD_AND_SET_LAYER);
-            PropertyValuesHolder panelAlpha = PropertyValuesHolder.ofFloat("alpha", initAlpha, 1f);
-            PropertyValuesHolder panelDriftY =
-                    PropertyValuesHolder.ofFloat("translationY", yDrift, 0);
-            PropertyValuesHolder panelDriftX =
-                    PropertyValuesHolder.ofFloat("translationX", xDrift, 0);
-
-            ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView,
-                    panelAlpha, panelDriftY, panelDriftX);
-
-            panelAlphaAndDrift.setDuration(revealDuration);
-            panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
-
-            mStateAnimation.play(panelAlphaAndDrift);
-
-            if (page != null) {
-                page.setVisibility(View.VISIBLE);
-                layerViews.put(page, BUILD_AND_SET_LAYER);
-
-                ObjectAnimator pageDrift = ObjectAnimator.ofFloat(page, "translationY", yDrift, 0);
-                page.setTranslationY(yDrift);
-                pageDrift.setDuration(revealDuration);
-                pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
-                pageDrift.setStartDelay(itemsAlphaStagger);
-                mStateAnimation.play(pageDrift);
-
-                page.setAlpha(0f);
-                ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(page, "alpha", 0f, 1f);
-                itemsAlpha.setDuration(revealDuration);
-                itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
-                itemsAlpha.setStartDelay(itemsAlphaStagger);
-                mStateAnimation.play(itemsAlpha);
-            }
-
-            View pageIndicators = toView.findViewById(R.id.apps_customize_page_indicator);
-            pageIndicators.setAlpha(0.01f);
-            ObjectAnimator indicatorsAlpha =
-                    ObjectAnimator.ofFloat(pageIndicators, "alpha", 1f);
-            indicatorsAlpha.setDuration(revealDuration);
-            mStateAnimation.play(indicatorsAlpha);
-
-            if (material) {
-                final View allApps = getAllAppsButton();
-                int allAppsButtonSize = LauncherAppState.getInstance().
-                        getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize;
-                float startRadius = isWidgetTray ? 0 : allAppsButtonSize / 2;
-                Animator reveal = ViewAnimationUtils.createCircularReveal(revealView, width / 2,
-                                height / 2, startRadius, revealRadius);
-                reveal.setDuration(revealDuration);
-                reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
-
-                reveal.addListener(new AnimatorListenerAdapter() {
-                    public void onAnimationStart(Animator animation) {
-                        if (!isWidgetTray) {
-                            allApps.setVisibility(View.INVISIBLE);
-                        }
-                    }
-                    public void onAnimationEnd(Animator animation) {
-                        if (!isWidgetTray) {
-                            allApps.setVisibility(View.VISIBLE);
-                        }
-                    }
-                });
-                mStateAnimation.play(reveal);
-            }
-
-            mStateAnimation.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    dispatchOnLauncherTransitionEnd(fromView, animated, false);
-                    dispatchOnLauncherTransitionEnd(toView, animated, false);
-
-                    revealView.setVisibility(View.INVISIBLE);
-
-                    for (View v : layerViews.keySet()) {
-                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
-                            v.setLayerType(View.LAYER_TYPE_NONE, null);
-                        }
-                    }
-                    content.setPageBackgroundsVisible(true);
-
-                    // Hide the search bar
-                    if (mSearchDropTargetBar != null) {
-                        mSearchDropTargetBar.hideSearchBar(false);
-                    }
-
-                    // This can hold unnecessary references to views.
-                    mStateAnimation = null;
-                }
-
-            });
-
-            if (workspaceAnim != null) {
-                mStateAnimation.play(workspaceAnim);
-            }
-
-            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
-            dispatchOnLauncherTransitionPrepare(toView, animated, false);
-            final AnimatorSet stateAnimation = mStateAnimation;
-            final Runnable startAnimRunnable = new Runnable() {
-                public void run() {
-                    // Check that mStateAnimation hasn't changed while
-                    // we waited for a layout/draw pass
-                    if (mStateAnimation != stateAnimation)
-                        return;
-                    dispatchOnLauncherTransitionStart(fromView, animated, false);
-                    dispatchOnLauncherTransitionStart(toView, animated, false);
-
-                    revealView.setAlpha(initAlpha);
-
-                    for (View v : layerViews.keySet()) {
-                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
-                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-                        }
-                    }
-
-                    if (Utilities.isLmpOrAbove()) {
-                        for (View v : layerViews.keySet()) {
-                            if (Utilities.isViewAttachedToWindow(v)) v.buildLayer();
-                        }
-                    }
-                    mStateAnimation.start();
-                }
-            };
-            toView.bringToFront();
-            toView.setVisibility(View.VISIBLE);
-            toView.post(startAnimRunnable);
-        } else {
-            toView.setTranslationX(0.0f);
-            toView.setTranslationY(0.0f);
-            toView.setScaleX(1.0f);
-            toView.setScaleY(1.0f);
-            toView.setVisibility(View.VISIBLE);
-            toView.bringToFront();
-
-            if (!springLoaded && !LauncherAppState.getInstance().isScreenLarge()) {
-                // Hide the search bar
-                if (mSearchDropTargetBar != null) {
-                    mSearchDropTargetBar.hideSearchBar(false);
-                }
-            }
-            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
-            dispatchOnLauncherTransitionStart(fromView, animated, false);
-            dispatchOnLauncherTransitionEnd(fromView, animated, false);
-            dispatchOnLauncherTransitionPrepare(toView, animated, false);
-            dispatchOnLauncherTransitionStart(toView, animated, false);
-            dispatchOnLauncherTransitionEnd(toView, animated, false);
-        }
-    }
-
-    /**
-     * Zoom the camera back into the workspace, hiding 'fromView'.
-     * This is the opposite of showAppsCustomizeHelper.
-     * @param animated If true, the transition will be animated.
-     */
-    private void hideAppsCustomizeHelper(Workspace.State toState, final boolean animated,
-            final boolean springLoaded, final Runnable onCompleteRunnable) {
-
-        if (mStateAnimation != null) {
-            mStateAnimation.setDuration(0);
-            mStateAnimation.cancel();
-            mStateAnimation = null;
-        }
-
-        boolean material = Utilities.isLmpOrAbove();
-        Resources res = getResources();
-
-        final int revealDuration = res.getInteger(R.integer.config_appsCustomizeConcealTime);
-        final int itemsAlphaStagger =
-                res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger);
-
-        final View fromView = mAppsCustomizeTabHost;
-        final View toView = mWorkspace;
-        Animator workspaceAnim = null;
-        final HashMap<View, Integer> layerViews = new HashMap<View, Integer>();
-
-        if (toState == Workspace.State.NORMAL) {
-            workspaceAnim = mWorkspace.getChangeStateAnimation(
-                    toState, animated, layerViews);
-        } else if (toState == Workspace.State.SPRING_LOADED ||
-                toState == Workspace.State.OVERVIEW) {
-            workspaceAnim = mWorkspace.getChangeStateAnimation(
-                    toState, animated, layerViews);
-        }
-
-        // If for some reason our views aren't initialized, don't animate
-        boolean initialized = getAllAppsButton() != null;
-
-        if (animated && initialized) {
-            mStateAnimation = LauncherAnimUtils.createAnimatorSet();
-            if (workspaceAnim != null) {
-                mStateAnimation.play(workspaceAnim);
-            }
-
-            final AppsCustomizePagedView content = (AppsCustomizePagedView)
-                    fromView.findViewById(R.id.apps_customize_pane_content);
-
-            final View page = content.getPageAt(content.getNextPage());
-
-            // We need to hide side pages of the Apps / Widget tray to avoid some ugly edge cases
-            int count = content.getChildCount();
-            for (int i = 0; i < count; i++) {
-                View child = content.getChildAt(i);
-                if (child != page) {
-                    child.setVisibility(View.INVISIBLE);
-                }
-            }
-            final View revealView = fromView.findViewById(R.id.fake_page);
-
-            // hideAppsCustomizeHelper is called in some cases when it is already hidden
-            // don't perform all these no-op animations. In particularly, this was causing
-            // the all-apps button to pop in and out.
-            if (fromView.getVisibility() == View.VISIBLE) {
-                AppsCustomizePagedView.ContentType contentType = content.getContentType();
-                final boolean isWidgetTray =
-                        contentType == AppsCustomizePagedView.ContentType.Widgets;
-
-                if (isWidgetTray) {
-                    revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark));
-                } else {
-                    revealView.setBackground(res.getDrawable(R.drawable.quantum_panel));
-                }
-
-                int width = revealView.getMeasuredWidth();
-                int height = revealView.getMeasuredHeight();
-                float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4);
-
-                // Hide the real page background, and swap in the fake one
-                revealView.setVisibility(View.VISIBLE);
-                content.setPageBackgroundsVisible(false);
-
-                final View allAppsButton = getAllAppsButton();
-                revealView.setTranslationY(0);
-                int[] allAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
-                        allAppsButton, null);
-
-                float xDrift = 0;
-                float yDrift = 0;
-                if (material) {
-                    yDrift = isWidgetTray ? height / 2 : allAppsToPanelDelta[1];
-                    xDrift = isWidgetTray ? 0 : allAppsToPanelDelta[0];
-                } else {
-                    yDrift = 2 * height / 3;
-                    xDrift = 0;
-                }
-
-                layerViews.put(revealView, BUILD_AND_SET_LAYER);
-                TimeInterpolator decelerateInterpolator = material ?
-                        new LogDecelerateInterpolator(100, 0) :
-                        new DecelerateInterpolator(1f);
-
-                // The vertical motion of the apps panel should be delayed by one frame
-                // from the conceal animation in order to give the right feel. We correpsondingly
-                // shorten the duration so that the slide and conceal end at the same time.
-                ObjectAnimator panelDriftY = LauncherAnimUtils.ofFloat(revealView, "translationY",
-                        0, yDrift);
-                panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY);
-                panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
-                panelDriftY.setInterpolator(decelerateInterpolator);
-                mStateAnimation.play(panelDriftY);
-
-                ObjectAnimator panelDriftX = LauncherAnimUtils.ofFloat(revealView, "translationX",
-                        0, xDrift);
-                panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY);
-                panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
-                panelDriftX.setInterpolator(decelerateInterpolator);
-                mStateAnimation.play(panelDriftX);
-
-                if (isWidgetTray || !material) {
-                    float finalAlpha = material ? 0.4f : 0f;
-                    revealView.setAlpha(1f);
-                    ObjectAnimator panelAlpha = LauncherAnimUtils.ofFloat(revealView, "alpha",
-                            1f, finalAlpha);
-                    panelAlpha.setDuration(material ? revealDuration : 150);
-                    panelAlpha.setInterpolator(decelerateInterpolator);
-                    panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY);
-                    mStateAnimation.play(panelAlpha);
-                }
-
-                if (page != null) {
-                    layerViews.put(page, BUILD_AND_SET_LAYER);
-
-                    ObjectAnimator pageDrift = LauncherAnimUtils.ofFloat(page, "translationY",
-                            0, yDrift);
-                    page.setTranslationY(0);
-                    pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY);
-                    pageDrift.setInterpolator(decelerateInterpolator);
-                    pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
-                    mStateAnimation.play(pageDrift);
-
-                    page.setAlpha(1f);
-                    ObjectAnimator itemsAlpha = LauncherAnimUtils.ofFloat(page, "alpha", 1f, 0f);
-                    itemsAlpha.setDuration(100);
-                    itemsAlpha.setInterpolator(decelerateInterpolator);
-                    mStateAnimation.play(itemsAlpha);
-                }
-
-                View pageIndicators = fromView.findViewById(R.id.apps_customize_page_indicator);
-                pageIndicators.setAlpha(1f);
-                ObjectAnimator indicatorsAlpha =
-                        LauncherAnimUtils.ofFloat(pageIndicators, "alpha", 0f);
-                indicatorsAlpha.setDuration(revealDuration);
-                indicatorsAlpha.setInterpolator(new DecelerateInterpolator(1.5f));
-                mStateAnimation.play(indicatorsAlpha);
-
-                width = revealView.getMeasuredWidth();
-
-                if (material) {
-                    if (!isWidgetTray) {
-                        allAppsButton.setVisibility(View.INVISIBLE);
-                    }
-                    int allAppsButtonSize = LauncherAppState.getInstance().
-                            getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize;
-                    float finalRadius = isWidgetTray ? 0 : allAppsButtonSize / 2;
-                    Animator reveal =
-                            LauncherAnimUtils.createCircularReveal(revealView, width / 2,
-                                    height / 2, revealRadius, finalRadius);
-                    reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
-                    reveal.setDuration(revealDuration);
-                    reveal.setStartDelay(itemsAlphaStagger);
-
-                    reveal.addListener(new AnimatorListenerAdapter() {
-                        public void onAnimationEnd(Animator animation) {
-                            revealView.setVisibility(View.INVISIBLE);
-                            if (!isWidgetTray) {
-                                allAppsButton.setVisibility(View.VISIBLE);
-                            }
-                        }
-                    });
-
-                    mStateAnimation.play(reveal);
-                }
-
-                dispatchOnLauncherTransitionPrepare(fromView, animated, true);
-                dispatchOnLauncherTransitionPrepare(toView, animated, true);
-                mAppsCustomizeContent.stopScrolling();
-            }
-
-            mStateAnimation.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    fromView.setVisibility(View.GONE);
-                    dispatchOnLauncherTransitionEnd(fromView, animated, true);
-                    dispatchOnLauncherTransitionEnd(toView, animated, true);
-                    if (onCompleteRunnable != null) {
-                        onCompleteRunnable.run();
-                    }
-
-                    for (View v : layerViews.keySet()) {
-                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
-                            v.setLayerType(View.LAYER_TYPE_NONE, null);
-                        }
-                    }
-
-                    content.setPageBackgroundsVisible(true);
-                    // Unhide side pages
-                    int count = content.getChildCount();
-                    for (int i = 0; i < count; i++) {
-                        View child = content.getChildAt(i);
-                        child.setVisibility(View.VISIBLE);
-                    }
-
-                    // Reset page transforms
-                    if (page != null) {
-                        page.setTranslationX(0);
-                        page.setTranslationY(0);
-                        page.setAlpha(1);
-                    }
-                    content.setCurrentPage(content.getNextPage());
-
-                    mAppsCustomizeContent.updateCurrentPageScroll();
-
-                    // This can hold unnecessary references to views.
-                    mStateAnimation = null;
-                }
-            });
-
-            final AnimatorSet stateAnimation = mStateAnimation;
-            final Runnable startAnimRunnable = new Runnable() {
-                public void run() {
-                    // Check that mStateAnimation hasn't changed while
-                    // we waited for a layout/draw pass
-                    if (mStateAnimation != stateAnimation)
-                        return;
-                    dispatchOnLauncherTransitionStart(fromView, animated, false);
-                    dispatchOnLauncherTransitionStart(toView, animated, false);
-
-                    for (View v : layerViews.keySet()) {
-                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
-                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
-                        }
-                    }
-
-                    if (Utilities.isLmpOrAbove()) {
-                        for (View v : layerViews.keySet()) {
-                            if (Utilities.isViewAttachedToWindow(v)) v.buildLayer();
-                        }
-                    }
-                    mStateAnimation.start();
-                }
-            };
-            fromView.post(startAnimRunnable);
-        } else {
-            fromView.setVisibility(View.GONE);
-            dispatchOnLauncherTransitionPrepare(fromView, animated, true);
-            dispatchOnLauncherTransitionStart(fromView, animated, true);
-            dispatchOnLauncherTransitionEnd(fromView, animated, true);
-            dispatchOnLauncherTransitionPrepare(toView, animated, true);
-            dispatchOnLauncherTransitionStart(toView, animated, true);
-            dispatchOnLauncherTransitionEnd(toView, animated, true);
-        }
-    }
-
     @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
@@ -3820,28 +3286,33 @@
             SQLiteDatabase.releaseMemory();
 
             // This clears all widget bitmaps from the widget tray
-            if (mAppsCustomizeTabHost != null) {
-                mAppsCustomizeTabHost.trimMemory();
-            }
+            // TODO(hyunyoungs)
         }
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onTrimMemory(level);
         }
     }
 
+    @Override
+    public void onStateTransitionHideSearchBar() {
+        // Hide the search bar
+        if (mSearchDropTargetBar != null) {
+            mSearchDropTargetBar.hideSearchBar(false /* animated */);
+        }
+    }
+
     protected void showWorkspace(boolean animated) {
         showWorkspace(animated, null);
     }
 
-    protected void showWorkspace() {
-        showWorkspace(true);
-    }
-
     void showWorkspace(boolean animated, Runnable onCompleteRunnable) {
-        if (mState != State.WORKSPACE || mWorkspace.getState() != Workspace.State.NORMAL) {
+        boolean changed = mState != State.WORKSPACE ||
+                mWorkspace.getState() != Workspace.State.NORMAL;
+        if (changed) {
             boolean wasInSpringLoadedMode = (mState != State.WORKSPACE);
             mWorkspace.setVisibility(View.VISIBLE);
-            hideAppsCustomizeHelper(Workspace.State.NORMAL, animated, false, onCompleteRunnable);
+            mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.NORMAL,
+                    animated, onCompleteRunnable);
 
             // Show the search bar (only animate if we were showing the drop target bar in spring
             // loaded mode)
@@ -3860,18 +3331,21 @@
 
         // Resume the auto-advance of widgets
         mUserPresent = true;
-        updateRunning();
+        updateAutoAdvanceState();
 
-        // Send an accessibility event to announce the context change
-        getWindow().getDecorView()
-                .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+        if (changed) {
+            // Send an accessibility event to announce the context change
+            getWindow().getDecorView()
+                    .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
 
-        onWorkspaceShown(animated);
+            onWorkspaceShown(animated);
+        }
     }
 
     void showOverviewMode(boolean animated) {
         mWorkspace.setVisibility(View.VISIBLE);
-        hideAppsCustomizeHelper(Workspace.State.OVERVIEW, animated, false, null);
+        mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.OVERVIEW,
+                animated, null /* onCompleteRunnable */);
         mState = State.WORKSPACE;
         onWorkspaceShown(animated);
     }
@@ -3879,28 +3353,56 @@
     public void onWorkspaceShown(boolean animated) {
     }
 
-    void showAllApps(boolean animated, AppsCustomizePagedView.ContentType contentType,
-                     boolean resetPageToZero) {
-        if (mState != State.WORKSPACE) return;
-
-        if (resetPageToZero) {
-            mAppsCustomizeTabHost.reset();
+    /**
+     * Shows the apps view.
+     */
+    void showAppsView(boolean animated, boolean resetListToTop) {
+        if (resetListToTop) {
+            mAppsView.scrollToTop();
         }
-        showAppsCustomizeHelper(animated, false, contentType);
-        mAppsCustomizeTabHost.post(new Runnable() {
+        showAppsOrWidgets(animated, State.APPS);
+    }
+
+    /**
+     * Shows the widgets view.
+     */
+    void showWidgetsView(boolean animated, boolean resetPageToZero) {
+        Log.d(TAG, "showWidgetsView:" + animated + " resetPageToZero:" + resetPageToZero);
+        if (resetPageToZero) {
+            mWidgetsView.scrollToTop();
+        }
+        showAppsOrWidgets(animated, State.WIDGETS);
+
+        mWidgetsView.post(new Runnable() {
             @Override
             public void run() {
-                // We post this in-case the all apps view isn't yet constructed.
-                mAppsCustomizeTabHost.requestFocus();
+                mWidgetsView.requestFocus();
             }
         });
+    }
+
+    /**
+     * Sets up the transition to show the apps/widgets view.
+     */
+    private void showAppsOrWidgets(boolean animated, State toState) {
+        if (mState != State.WORKSPACE) return;
+        if (toState != State.APPS && toState != State.WIDGETS) return;
+
+        if (toState == State.APPS) {
+            mStateTransitionAnimation.startAnimationToAllApps(animated);
+            if (mLauncherCallbacks != null) {
+                mLauncherCallbacks.onAllAppsShown();
+            }
+        } else {
+            mStateTransitionAnimation.startAnimationToWidgets(animated);
+        }
 
         // Change the state *after* we've called all the transition code
-        mState = State.APPS_CUSTOMIZE;
+        mState = toState;
 
         // Pause the auto-advance of widgets until we are out of AllApps
         mUserPresent = false;
-        updateRunning();
+        updateAutoAdvanceState();
         closeFolder();
 
         // Send an accessibility event to announce the context change
@@ -3908,25 +3410,33 @@
                 .sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
     }
 
-    void enterSpringLoadedDragMode() {
-        if (isAllAppsVisible()) {
-            hideAppsCustomizeHelper(Workspace.State.SPRING_LOADED, true, true, null);
-            mState = State.APPS_CUSTOMIZE_SPRING_LOADED;
+    public void enterSpringLoadedDragMode() {
+        Log.d(TAG, String.format("enterSpringLoadedDragMode [mState=%s",
+                mState.name()));
+        if (mState == State.WORKSPACE || mState == State.APPS_SPRING_LOADED ||
+                mState == State.WIDGETS_SPRING_LOADED) {
+            return;
         }
+
+        mStateTransitionAnimation.startAnimationToWorkspace(mState, Workspace.State.SPRING_LOADED,
+                true /* animated */, null /* onCompleteRunnable */);
+        mState = isAppsViewVisible() ? State.APPS_SPRING_LOADED : State.WIDGETS_SPRING_LOADED;
     }
 
-    void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay,
+    public void exitSpringLoadedDragModeDelayed(final boolean successfulDrop, int delay,
             final Runnable onCompleteRunnable) {
-        if (mState != State.APPS_CUSTOMIZE_SPRING_LOADED) return;
+        if (mState != State.APPS_SPRING_LOADED && mState != State.WIDGETS_SPRING_LOADED) return;
 
         mHandler.postDelayed(new Runnable() {
             @Override
             public void run() {
                 if (successfulDrop) {
+                    // TODO(hyunyoungs): verify if this hack is still needed, if not, delete.
+                    //
                     // Before we show workspace, hide all apps again because
                     // exitSpringLoadedDragMode made it visible. This is a bit hacky; we should
                     // clean up our state transition functions
-                    mAppsCustomizeTabHost.setVisibility(View.GONE);
+                    mWidgetsView.setVisibility(View.GONE);
                     showWorkspace(true, onCompleteRunnable);
                 } else {
                     exitSpringLoadedDragMode();
@@ -3936,11 +3446,12 @@
     }
 
     void exitSpringLoadedDragMode() {
-        if (mState == State.APPS_CUSTOMIZE_SPRING_LOADED) {
-            final boolean animated = true;
-            final boolean springLoaded = true;
-            showAppsCustomizeHelper(animated, springLoaded);
-            mState = State.APPS_CUSTOMIZE;
+        if (mState == State.APPS_SPRING_LOADED) {
+            mStateTransitionAnimation.startAnimationToAllApps(true /* animated */);
+            mState = State.APPS;
+        } else if (mState == State.WIDGETS_SPRING_LOADED) {
+            mStateTransitionAnimation.startAnimationToWidgets(true /* animated */);
+            mState = State.WIDGETS;
         }
         // Otherwise, we are not in spring loaded mode, so don't do anything.
     }
@@ -3957,7 +3468,7 @@
         // NO-OP
     }
 
-    public View getQsbBar() {
+    public View getOrCreateQsbBar() {
         if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) {
             return mLauncherCallbacks.getQsbBar();
         }
@@ -4006,19 +3517,31 @@
                 mQsb.updateAppWidgetOptions(opts);
                 mQsb.setPadding(0, 0, 0, 0);
                 mSearchDropTargetBar.addView(mQsb);
+                mSearchDropTargetBar.setQsbSearchBar(mQsb);
             }
         }
         return mQsb;
     }
 
+    private void reinflateQSBIfNecessary() {
+        if (mQsb instanceof LauncherAppWidgetHostView &&
+                ((LauncherAppWidgetHostView) mQsb).isReinflateRequired()) {
+            mSearchDropTargetBar.removeView(mQsb);
+            mQsb = null;
+            mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar());
+        }
+    }
+
     @Override
     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
         final boolean result = super.dispatchPopulateAccessibilityEvent(event);
         final List<CharSequence> text = event.getText();
         text.clear();
         // Populate event with a fake title based on the current state.
-        if (mState == State.APPS_CUSTOMIZE) {
-            text.add(mAppsCustomizeTabHost.getContentTag());
+        if (mState == State.APPS) {
+            text.add("Apps");
+        } else if (mState == State.WIDGETS) {
+            text.add("Widgets");
         } else {
             text.add(getString(R.string.all_apps_home_button_label));
         }
@@ -4028,7 +3551,7 @@
     /**
      * Receives notifications when system dialogs are to be closed.
      */
-    private class CloseSystemDialogsIntentReceiver extends BroadcastReceiver {
+    @Thunk class CloseSystemDialogsIntentReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
             closeSystemDialogs();
@@ -4241,9 +3764,8 @@
         // Remove the extra empty screen
         mWorkspace.removeExtraEmptyScreen(false, false);
 
-        if (!LauncherAppState.isDisableAllApps() &&
-                addedApps != null && mAppsCustomizeContent != null) {
-            mAppsCustomizeContent.addApps(addedApps);
+        if (addedApps != null && mAppsView != null) {
+            mAppsView.addApps(addedApps);
         }
     }
 
@@ -4416,8 +3938,8 @@
             pendingInfo.spanY = item.spanY;
             pendingInfo.minSpanX = item.minSpanX;
             pendingInfo.minSpanY = item.minSpanY;
-            Bundle options =
-                    AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo);
+            Bundle options = null;
+            //        AppsCustomizePagedView.getDefaultOptionsForWidget(this, pendingInfo);
 
             int newWidgetId = mAppWidgetHost.allocateAppWidgetId();
             boolean success = mAppWidgetManager.bindAppWidgetIdIfAllowed(
@@ -4510,10 +4032,10 @@
      *
      * Implementation of the method from LauncherModel.Callbacks.
      */
-    public void finishBindingItems(final boolean upgradePath) {
+    public void finishBindingItems() {
         Runnable r = new Runnable() {
             public void run() {
-                finishBindingItems(upgradePath);
+                finishBindingItems();
             }
         };
         if (waitUntilResume(r)) {
@@ -4548,14 +4070,10 @@
             sPendingAddItem = null;
         }
 
-        if (upgradePath) {
-            mWorkspace.getUniqueComponents(true, null);
-            mIntentsOnWorkspaceFromUpgradePath = mWorkspace.getUniqueComponents(true, null);
-        }
         PackageInstallerCompat.getInstance(this).onFinishBind();
 
         if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.finishBindingItems(upgradePath);
+            mLauncherCallbacks.finishBindingItems(false);
         }
     }
 
@@ -4612,7 +4130,7 @@
             mSearchDropTargetBar.removeView(mQsb);
             mQsb = null;
         }
-        mSearchDropTargetBar.setQsbSearchBar(getQsbBar());
+        mSearchDropTargetBar.setQsbSearchBar(getOrCreateQsbBar());
     }
 
     /**
@@ -4621,24 +4139,12 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void bindAllApplications(final ArrayList<AppInfo> apps) {
-        if (LauncherAppState.isDisableAllApps()) {
-            if (mIntentsOnWorkspaceFromUpgradePath != null) {
-                if (LauncherModel.UPGRADE_USE_MORE_APPS_FOLDER) {
-                    getHotseat().addAllAppsFolder(mIconCache, apps,
-                            mIntentsOnWorkspaceFromUpgradePath, Launcher.this, mWorkspace);
-                }
-                mIntentsOnWorkspaceFromUpgradePath = null;
-            }
-            if (mAppsCustomizeContent != null) {
-                mAppsCustomizeContent.onPackagesUpdated(
-                        LauncherModel.getSortedWidgetsAndShortcuts(this, false /* refresh */));
-            }
-        } else {
-            if (mAppsCustomizeContent != null) {
-                mAppsCustomizeContent.setApps(apps);
-                mAppsCustomizeContent.onPackagesUpdated(
-                        LauncherModel.getSortedWidgetsAndShortcuts(this, false /* refresh */));
-            }
+        if (mAppsView != null) {
+            mAppsView.setApps(apps);
+        }
+        if (mWidgetsView != null) {
+            mWidgetsView.addWidgets(LauncherModel.getSortedWidgetsAndShortcuts(this, false),
+                    getPackageManager());
         }
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.bindAllApplications(apps);
@@ -4660,9 +4166,8 @@
             return;
         }
 
-        if (!LauncherAppState.isDisableAllApps() &&
-                mAppsCustomizeContent != null) {
-            mAppsCustomizeContent.updateApps(apps);
+        if (mAppsView != null) {
+            mAppsView.updateApps(apps);
         }
     }
 
@@ -4776,31 +4281,31 @@
         }
 
         // Update AllApps
-        if (!LauncherAppState.isDisableAllApps() &&
-                mAppsCustomizeContent != null) {
-            mAppsCustomizeContent.removeApps(appInfos);
+        if (mAppsView != null) {
+            mAppsView.removeApps(appInfos);
         }
     }
 
     /**
      * A number of packages were updated.
      */
-    private ArrayList<Object> mWidgetsAndShortcuts;
+    @Thunk ArrayList<Object> mWidgetsAndShortcuts;
     private Runnable mBindPackagesUpdatedRunnable = new Runnable() {
             public void run() {
                 bindPackagesUpdated(mWidgetsAndShortcuts);
                 mWidgetsAndShortcuts = null;
             }
         };
+
     public void bindPackagesUpdated(final ArrayList<Object> widgetsAndShortcuts) {
         if (waitUntilResume(mBindPackagesUpdatedRunnable, true)) {
             mWidgetsAndShortcuts = widgetsAndShortcuts;
             return;
         }
 
-        // Update the widgets pane
-        if (mAppsCustomizeContent != null) {
-            mAppsCustomizeContent.onPackagesUpdated(widgetsAndShortcuts);
+        if (mWidgetsView != null) {
+            mWidgetsView.addWidgets(LauncherModel.getSortedWidgetsAndShortcuts(this, false),
+                    getPackageManager());
         }
     }
 
@@ -4910,6 +4415,17 @@
         return null;
     }
 
+    /**
+     * Returns whether the launcher callbacks overrides search in all apps.
+     * @return
+     */
+    @Thunk boolean isAllAppsSearchOverridden() {
+        if (mLauncherCallbacks != null) {
+            return mLauncherCallbacks.overrideAllAppsSearch();
+        }
+        return false;
+    }
+
     private boolean shouldRunFirstRunActivity() {
         return !ActivityManager.isRunningInTestHarness() &&
                 !mSharedPrefs.getBoolean(FIRST_RUN_ACTIVITY_DISPLAYED, false);
@@ -5010,7 +4526,7 @@
         editor.apply();
     }
 
-    private void showFirstRunClings() {
+    @Thunk void showFirstRunClings() {
         // The two first run cling paths are mutually exclusive, if the launcher is preinstalled
         // on the device, then we always show the first run cling experience (or if there is no
         // launcher2). Otherwise, we prompt the user upon started for migration
@@ -5047,7 +4563,7 @@
         if (activityInfo == null) {
             return null;
         }
-        return new AppInfo(this, activityInfo, myUser, mIconCache, null);
+        return new AppInfo(this, activityInfo, myUser, mIconCache);
     }
 
     public ItemInfo createShortcutDragInfo(Intent shortcutIntent, CharSequence caption,
@@ -5093,10 +4609,8 @@
         Log.d(TAG, "mSavedInstanceState=" + mSavedInstanceState);
         Log.d(TAG, "sFolders.size=" + sFolders.size());
         mModel.dumpState();
+        // TODO(hyunyoungs): add mWidgetsView.dumpState(); or mWidgetsModel.dumpState();
 
-        if (mAppsCustomizeContent != null) {
-            mAppsCustomizeContent.dumpState();
-        }
         Log.d(TAG, "END launcher3 dump state");
     }
 
diff --git a/src/com/android/launcher3/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/LauncherAccessibilityDelegate.java
index c9e277e..8ba02ea 100644
--- a/src/com/android/launcher3/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/LauncherAccessibilityDelegate.java
@@ -1,15 +1,21 @@
 package com.android.launcher3;
 
 import android.annotation.TargetApi;
+import android.content.res.Resources;
+import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
 import android.util.SparseArray;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import com.android.launcher3.LauncherModel.ScreenPosProvider;
+import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 
@@ -20,10 +26,25 @@
     public static final int INFO = R.id.action_info;
     public static final int UNINSTALL = R.id.action_uninstall;
     public static final int ADD_TO_WORKSPACE = R.id.action_add_to_workspace;
+    public static final int MOVE = R.id.action_move;
+
+    enum DragType {
+        ICON,
+        FOLDER,
+        WIDGET
+    }
+
+    public static class DragInfo {
+        DragType dragType;
+        ItemInfo info;
+        View item;
+    }
+
+    private DragInfo mDragInfo = null;
 
     private final SparseArray<AccessibilityAction> mActions =
             new SparseArray<AccessibilityAction>();
-    private final Launcher mLauncher;
+    @Thunk final Launcher mLauncher;
 
     public LauncherAccessibilityDelegate(Launcher launcher) {
         mLauncher = launcher;
@@ -36,6 +57,9 @@
                 launcher.getText(R.string.delete_target_uninstall_label)));
         mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE,
                 launcher.getText(R.string.action_add_to_workspace)));
+        mActions.put(MOVE, new AccessibilityAction(MOVE,
+                launcher.getText(R.string.action_move)));
+
     }
 
     @Override
@@ -49,6 +73,7 @@
                 || (item instanceof FolderInfo)) {
             // Workspace shortcut / widget
             info.addAction(mActions.get(REMOVE));
+            info.addAction(mActions.get(MOVE));
         } else if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) {
             // App or Widget from customization tray
             if (item instanceof AppInfo) {
@@ -69,14 +94,22 @@
     }
 
     public boolean performAction(View host, ItemInfo item, int action) {
+        Resources res = mLauncher.getResources();
         if (action == REMOVE) {
-            return DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host);
+            if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) {
+                announceConfirmation(R.string.item_removed_from_workspace);
+                return true;
+            }
+            return false;
         } else if (action == INFO) {
             InfoDropTarget.startDetailsActivityForInfo(item, mLauncher);
             return true;
         } else if (action == UNINSTALL) {
-            DeleteDropTarget.uninstallApp(mLauncher, (AppInfo) item);
+            AppInfo info = (AppInfo) item;
+            mLauncher.startApplicationUninstallActivity(info.componentName, info.flags, info.user);
             return true;
+        } else if (action == MOVE) {
+            beginAccessibleDrag(host, item);
         } else if (action == ADD_TO_WORKSPACE) {
             final int preferredPage = mLauncher.getWorkspace().getCurrentPage();
             final ScreenPosProvider screenProvider = new ScreenPosProvider() {
@@ -90,20 +123,92 @@
                 final ArrayList<ItemInfo> addShortcuts = new ArrayList<ItemInfo>();
                 addShortcuts.add(((AppInfo) item).makeShortcut());
                 mLauncher.showWorkspace(true, new Runnable() {
-
                     @Override
                     public void run() {
-                        mLauncher.getModel().addAndBindAddedWorkspaceApps(
+                        mLauncher.getModel().addAndBindAddedWorkspaceItems(
                                 mLauncher, addShortcuts, screenProvider, 0, true);
+                        announceConfirmation(R.string.item_added_to_workspace);
                     }
                 });
                 return true;
             } else if (item instanceof PendingAddItemInfo) {
                 mLauncher.getModel().addAndBindPendingItem(
                         mLauncher, (PendingAddItemInfo) item, screenProvider, 0);
+                announceConfirmation(R.string.item_added_to_workspace);
                 return true;
             }
         }
         return false;
     }
+
+    @Thunk void announceConfirmation(int resId) {
+        announceConfirmation(mLauncher.getResources().getString(resId));
+    }
+
+    @Thunk void announceConfirmation(String confirmation) {
+        mLauncher.getDragLayer().announceForAccessibility(confirmation);
+
+    }
+
+    public boolean isInAccessibleDrag() {
+        return mDragInfo != null;
+    }
+
+    public DragInfo getDragInfo() {
+        return mDragInfo;
+    }
+
+    public void handleAccessibleDrop(CellLayout targetContainer, Rect dropLocation,
+            String confirmation) {
+        if (!isInAccessibleDrag()) return;
+
+        int[] loc = new int[2];
+        loc[0] = dropLocation.centerX();
+        loc[1] = dropLocation.centerY();
+
+        mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(targetContainer, loc);
+        mLauncher.getDragController().completeAccessibleDrag(loc);
+
+        endAccessibleDrag();
+        announceConfirmation(confirmation);
+    }
+
+    public void beginAccessibleDrag(View item, ItemInfo info) {
+        mDragInfo = new DragInfo();
+        mDragInfo.info = info;
+        mDragInfo.item = item;
+        mDragInfo.dragType = DragType.ICON;
+        if (info instanceof FolderInfo) {
+            mDragInfo.dragType = DragType.FOLDER;
+        } else if (info instanceof LauncherAppWidgetInfo) {
+            mDragInfo.dragType = DragType.WIDGET;
+        }
+
+        CellLayout.CellInfo cellInfo = new CellLayout.CellInfo(item, info);
+
+        Rect pos = new Rect();
+        mLauncher.getDragLayer().getDescendantRectRelativeToSelf(item, pos);
+
+        mLauncher.getDragController().prepareAccessibleDrag(pos.centerX(), pos.centerY());
+        mLauncher.getWorkspace().enableAccessibleDrag(true);
+        mLauncher.getWorkspace().startDrag(cellInfo, true);
+    }
+
+    public boolean onBackPressed() {
+        if (isInAccessibleDrag()) {
+            cancelAccessibleDrag();
+            return true;
+        }
+        return false;
+    }
+
+    private void cancelAccessibleDrag() {
+        mLauncher.getDragController().cancelDrag();
+        endAccessibleDrag();
+    }
+
+    private void endAccessibleDrag() {
+        mDragInfo = null;
+        mLauncher.getWorkspace().enableAccessibleDrag(false);
+    }
 }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 87e9aae..6e77d06 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -19,44 +19,38 @@
 import android.annotation.TargetApi;
 import android.app.SearchManager;
 import android.content.ComponentName;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.database.ContentObserver;
 import android.graphics.Point;
 import android.os.Build;
-import android.os.Handler;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.Display;
-import android.view.View.AccessibilityDelegate;
 import android.view.WindowManager;
 
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
+import com.android.launcher3.util.Thunk;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 
 public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks {
-    private static final String TAG = "LauncherAppState";
-
-    private static final boolean DEBUG = false;
 
     private final AppFilter mAppFilter;
     private final BuildInfo mBuildInfo;
-    private final LauncherModel mModel;
+    @Thunk final LauncherModel mModel;
     private final IconCache mIconCache;
+    private final WidgetPreviewLoader mWidgetCache;
 
     private final boolean mIsScreenLarge;
     private final float mScreenDensity;
     private final int mLongPressTimeout = 300;
 
-    private WidgetPreviewLoader.CacheDb mWidgetPreviewCacheDb;
     private boolean mWallpaperChangedSinceLastCheck;
 
     private static WeakReference<LauncherProvider> sLauncherProvider;
@@ -65,7 +59,7 @@
     private static LauncherAppState INSTANCE;
 
     private DynamicGrid mDynamicGrid;
-    private AccessibilityDelegate mAccessibilityDelegate;
+    private LauncherAccessibilityDelegate mAccessibilityDelegate;
 
     public static LauncherAppState getInstance() {
         if (INSTANCE == null) {
@@ -103,39 +97,22 @@
         // set sIsScreenXLarge and mScreenDensity *before* creating icon cache
         mIsScreenLarge = isScreenLarge(sContext.getResources());
         mScreenDensity = sContext.getResources().getDisplayMetrics().density;
-
-        recreateWidgetPreviewDb();
         mIconCache = new IconCache(sContext);
+        mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);
 
         mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
         mBuildInfo = BuildInfo.loadByName(sContext.getString(R.string.build_info_class));
         mModel = new LauncherModel(this, mIconCache, mAppFilter);
-        final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext);
-        launcherApps.addOnAppsChangedCallback(mModel);
+
+        LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel);
 
         // Register intent receivers
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_LOCALE_CHANGED);
         filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
-        sContext.registerReceiver(mModel, filter);
-        filter = new IntentFilter();
         filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
-        sContext.registerReceiver(mModel, filter);
-        filter = new IntentFilter();
         filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
         sContext.registerReceiver(mModel, filter);
-
-        // Register for changes to the favorites
-        ContentResolver resolver = sContext.getContentResolver();
-        resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
-                mFavoritesObserver);
-    }
-
-    public void recreateWidgetPreviewDb() {
-        if (mWidgetPreviewCacheDb != null) {
-            mWidgetPreviewCacheDb.close();
-        }
-        mWidgetPreviewCacheDb = new WidgetPreviewLoader.CacheDb(sContext);
     }
 
     /**
@@ -146,23 +123,16 @@
         final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(sContext);
         launcherApps.removeOnAppsChangedCallback(mModel);
         PackageInstallerCompat.getInstance(sContext).onStop();
-
-        ContentResolver resolver = sContext.getContentResolver();
-        resolver.unregisterContentObserver(mFavoritesObserver);
     }
 
     /**
-     * Receives notifications whenever the user favorites have changed.
+     * Reloads the workspace items from the DB and re-binds the workspace. This should generally
+     * not be called as DB updates are automatically followed by UI update
      */
-    private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) {
-        @Override
-        public void onChange(boolean selfChange) {
-            // If the database has ever changed, then we really need to force a reload of the
-            // workspace on the next load
-            mModel.resetLoadedState(false, true);
-            mModel.startLoaderFromBackground();
-        }
-    };
+    public void reloadWorkspace() {
+        mModel.resetLoadedState(false, true);
+        mModel.startLoaderFromBackground();
+    }
 
     LauncherModel setLauncher(Launcher launcher) {
         mModel.initialize(launcher);
@@ -171,7 +141,7 @@
         return mModel;
     }
 
-    AccessibilityDelegate getAccessibilityDelegate() {
+    public LauncherAccessibilityDelegate getAccessibilityDelegate() {
         return mAccessibilityDelegate;
     }
 
@@ -179,7 +149,7 @@
         return mIconCache;
     }
 
-    LauncherModel getModel() {
+    public LauncherModel getModel() {
         return mModel;
     }
 
@@ -187,10 +157,6 @@
         return mAppFilter == null || mAppFilter.shouldShowApp(componentName);
     }
 
-    WidgetPreviewLoader.CacheDb getWidgetPreviewCacheDb() {
-        return mWidgetPreviewCacheDb;
-    }
-
     static void setLauncherProvider(LauncherProvider provider) {
         sLauncherProvider = new WeakReference<LauncherProvider>(provider);
     }
@@ -246,6 +212,10 @@
         return mDynamicGrid;
     }
 
+    public WidgetPreviewLoader getWidgetCache() {
+        return mWidgetCache;
+    }
+
     public boolean isScreenLarge() {
         return mIsScreenLarge;
     }
@@ -283,12 +253,6 @@
         Utilities.setIconSize(grid.iconSizePx);
     }
 
-    public static boolean isDisableAllApps() {
-        // Returns false on non-dogfood builds.
-        return getInstance().mBuildInfo.isDogfoodBuild() &&
-                Utilities.isPropertyEnabled(Launcher.DISABLE_ALL_APPS_PROPERTY);
-    }
-
     public static boolean isDogfoodBuild() {
         return getInstance().mBuildInfo.isDogfoodBuild();
     }
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
index 55596e5..583f85a 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -81,9 +81,13 @@
     protected void onProvidersChanged() {
         // Once we get the message that widget packages are updated, we need to rebind items
         // in AppsCustomize accordingly.
-        mLauncher.getModel().loadAndBindWidgetsAndShortcuts(mLauncher, mLauncher);
-        for (Runnable callback : mProviderChangeListeners) {
-            callback.run();
+        mLauncher.bindPackagesUpdated(LauncherModel.getSortedWidgetsAndShortcuts(mLauncher,
+                true /* refresh */));
+
+        if (!mProviderChangeListeners.isEmpty()) {
+            for (Runnable callback : new ArrayList<>(mProviderChangeListeners)) {
+                callback.run();
+            }
         }
     }
 
diff --git a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
index e7f49b2..bb4580c 100644
--- a/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
+++ b/src/com/android/launcher3/LauncherAppWidgetProviderInfo.java
@@ -7,8 +7,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 
-import java.lang.reflect.Field;
-
 /**
  * This class is a thin wrapper around the framework AppWidgetProviderInfo class. This class affords
  * a common object for describing both framework provided AppWidgets as well as custom widgets
@@ -18,10 +16,10 @@
 public class LauncherAppWidgetProviderInfo extends AppWidgetProviderInfo {
 
     public boolean isCustomWidget = false;
-    int spanX = -1;
-    int spanY = -1;
-    int minSpanX = -1;
-    int minSpanY = -1;
+    public int spanX = -1;
+    public int spanY = -1;
+    public int minSpanX = -1;
+    public int minSpanY = -1;
 
     public static LauncherAppWidgetProviderInfo fromProviderInfo(Context context,
             AppWidgetProviderInfo info) {
@@ -80,10 +78,11 @@
         return super.loadIcon(context, cache.getFullResIconDpi());
     }
 
-    public String toString() {
+    public String toString(PackageManager pm) {
         if (isCustomWidget) {
-            return "LauncherAppWidgetProviderInfo(" + provider + ")";
+            return "WidgetProviderInfo(" + provider + ")";
         }
-        return super.toString();
+        return String.format("WidgetProviderInfo provider:%s package:%s short:%s label:%s span(%d, %d) minSpan(%d, %d)",
+                provider.toString(), provider.getPackageName(), provider.getShortClassName(), getLabel(pm), spanX, spanY, minSpanX, minSpanY);
     }
  }
diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java
index ddfd70d..5f7173f 100644
--- a/src/com/android/launcher3/LauncherBackupAgentHelper.java
+++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java
@@ -78,7 +78,7 @@
             super.onRestore(data, appVersionCode, newState);
             // If no favorite was migrated, clear the data and start fresh.
             final Cursor c = getContentResolver().query(
-                    LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, null, null, null, null);
+                    LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
             hasData = c.moveToNext();
             c.close();
         } catch (Exception e) {
diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java
index 31b434c..064f436 100644
--- a/src/com/android/launcher3/LauncherBackupHelper.java
+++ b/src/com/android/launcher3/LauncherBackupHelper.java
@@ -19,13 +19,16 @@
 import android.app.backup.BackupDataOutput;
 import android.app.backup.BackupHelper;
 import android.app.backup.BackupManager;
-import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.XmlResourceParser;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -51,6 +54,9 @@
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 import com.google.protobuf.nano.MessageNano;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.ByteArrayOutputStream;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -58,7 +64,6 @@
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.zip.CRC32;
 
@@ -138,6 +143,8 @@
     private final Context mContext;
     private final HashSet<String> mExistingKeys;
     private final ArrayList<Key> mKeys;
+    private final ItemTypeMatcher[] mItemTypeMatchers;
+    private final long mUserSerial;
 
     private IconCache mIconCache;
     private BackupManager mBackupManager;
@@ -154,6 +161,10 @@
         mExistingKeys = new HashSet<String>();
         mKeys = new ArrayList<Key>();
         restoreSuccessful = true;
+        mItemTypeMatchers = new ItemTypeMatcher[CommonAppTypeParser.SUPPORTED_TYPE_COUNT];
+
+        UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
+        mUserSerial = userManager.getSerialNumberForUser(UserHandleCompat.myUserHandle());
     }
 
     private void dataChanged() {
@@ -290,6 +301,12 @@
         if (!restoreSuccessful) {
             return;
         }
+        if (!initializeIconCache()) {
+            // During restore we do not need an initialized instance of IconCache. We can create
+            // a temporary icon cache here, as the process will be rebooted after restore
+            // is complete.
+            mIconCache = new IconCache(mContext);
+        }
 
         int dataSize = data.size();
         if (mBuffer.length < dataSize) {
@@ -445,7 +462,7 @@
 
         ContentResolver cr = mContext.getContentResolver();
         ContentValues values = unpackFavorite(buffer, dataSize);
-        cr.insert(Favorites.CONTENT_URI_NO_NOTIFICATION, values);
+        cr.insert(Favorites.CONTENT_URI, values);
     }
 
     /**
@@ -594,7 +611,8 @@
             Log.w(TAG, "failed to unpack icon for " + key.name);
         }
         if (VERBOSE) Log.v(TAG, "saving restored icon as: " + key.name);
-        IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(key.name), icon, res.dpi);
+        mIconCache.preloadIcon(ComponentName.unflattenFromString(key.name), icon, res.dpi,
+                "" /* label */, mUserSerial);
     }
 
     /**
@@ -612,8 +630,7 @@
             return;
         }
         final ContentResolver cr = mContext.getContentResolver();
-        final WidgetPreviewLoader previewLoader = new WidgetPreviewLoader(mContext);
-        final PagedViewCellLayout widgetSpacingLayout = new PagedViewCellLayout(mContext);
+        final WidgetPreviewLoader previewLoader = appState.getWidgetCache();
         final int dpi = mContext.getResources().getDisplayMetrics().densityDpi;
         final DeviceProfile profile = appState.getDynamicGrid().getDeviceProfile();
         if (DEBUG) Log.d(TAG, "cellWidthPx: " + profile.cellWidthPx);
@@ -629,7 +646,6 @@
                 final long id = cursor.getLong(ID_INDEX);
                 final String providerName = cursor.getString(APPWIDGET_PROVIDER_INDEX);
                 final int spanX = cursor.getInt(SPANX_INDEX);
-                final int spanY = cursor.getInt(SPANY_INDEX);
                 final ComponentName provider = ComponentName.unflattenFromString(providerName);
                 Key key = null;
                 String backupKey = null;
@@ -648,11 +664,10 @@
                     if (DEBUG) Log.d(TAG, "I can count this high: " + backupWidgetCount);
                     if (backupWidgetCount < MAX_WIDGETS_PER_PASS) {
                         if (DEBUG) Log.d(TAG, "saving widget " + backupKey);
-                        previewLoader.setPreviewSize(spanX * profile.cellWidthPx,
-                                spanY * profile.cellHeightPx, widgetSpacingLayout);
                         UserHandleCompat user = UserHandleCompat.myUserHandle();
                         writeRowToBackup(key,
-                                packWidget(dpi, previewLoader, mIconCache, provider, user),
+                                packWidget(dpi, previewLoader,spanX * profile.cellWidthPx,
+                                        mIconCache, provider, user),
                                 data);
                         mKeys.add(key);
                         backupWidgetCount ++;
@@ -689,8 +704,8 @@
             if (icon == null) {
                 Log.w(TAG, "failed to unpack widget icon for " + key.name);
             } else {
-                IconCache.preloadIcon(mContext, ComponentName.unflattenFromString(widget.provider),
-                        icon, widget.icon.dpi);
+                mIconCache.preloadIcon(ComponentName.unflattenFromString(widget.provider),
+                        icon, widget.icon.dpi, widget.label, mUserSerial);
             }
         }
 
@@ -756,6 +771,17 @@
         return checksum.getValue();
     }
 
+    /**
+     * @return true if its an hotseat item, that can be replaced during restore.
+     * TODO: Extend check for folders in hotseat.
+     */
+    private boolean isReplaceableHotseatItem(Favorite favorite) {
+        return favorite.container == Favorites.CONTAINER_HOTSEAT
+                && favorite.intent != null
+                && (favorite.itemType == Favorites.ITEM_TYPE_APPLICATION
+                || favorite.itemType == Favorites.ITEM_TYPE_SHORTCUT);
+    }
+
     /** Serialize a Favorite for persistence, including a checksum wrapper. */
     private Favorite packFavorite(Cursor c) {
         Favorite favorite = new Favorite();
@@ -788,9 +814,10 @@
             favorite.title = title;
         }
         String intentDescription = c.getString(INTENT_INDEX);
+        Intent intent = null;
         if (!TextUtils.isEmpty(intentDescription)) {
             try {
-                Intent intent = Intent.parseUri(intentDescription, 0);
+                intent = Intent.parseUri(intentDescription, 0);
                 intent.removeExtra(ItemInfo.EXTRA_PROFILE);
                 favorite.intent = intent.toUri(0);
             } catch (URISyntaxException e) {
@@ -806,6 +833,31 @@
             }
         }
 
+        if (isReplaceableHotseatItem(favorite)) {
+            if (intent != null && intent.getComponent() != null) {
+                PackageManager pm = mContext.getPackageManager();
+                ActivityInfo activity = null;;
+                try {
+                    activity = pm.getActivityInfo(intent.getComponent(), 0);
+                } catch (NameNotFoundException e) {
+                    Log.e(TAG, "Target not found", e);
+                }
+                if (activity == null) {
+                    return favorite;
+                }
+                for (int i = 0; i < mItemTypeMatchers.length; i++) {
+                    if (mItemTypeMatchers[i] == null) {
+                        mItemTypeMatchers[i] = new ItemTypeMatcher(
+                                CommonAppTypeParser.getResourceForItemType(i));
+                    }
+                    if (mItemTypeMatchers[i].matches(activity, pm)) {
+                        favorite.targetType = i;
+                        break;
+                    }
+                }
+            }
+        }
+
         return favorite;
     }
 
@@ -813,6 +865,7 @@
     private ContentValues unpackFavorite(byte[] buffer, int dataSize)
             throws IOException {
         Favorite favorite = unpackProto(new Favorite(), buffer, dataSize);
+
         ContentValues values = new ContentValues();
         values.put(Favorites._ID, favorite.id);
         values.put(Favorites.SCREEN, favorite.screen);
@@ -863,8 +916,17 @@
                 throw new InvalidBackupException("Widget not in screen bounds, aborting restore");
             }
         } else {
-            // Let LauncherModel know we've been here.
-            values.put(LauncherSettings.Favorites.RESTORED, 1);
+            // Check if it is an hotseat item, that can be replaced.
+            if (isReplaceableHotseatItem(favorite)
+                    && favorite.targetType != Favorite.TARGET_NONE
+                    && favorite.targetType < CommonAppTypeParser.SUPPORTED_TYPE_COUNT) {
+                Log.e(TAG, "Added item type flag");
+                values.put(LauncherSettings.Favorites.RESTORED,
+                        1 | CommonAppTypeParser.encodeItemTypeToFlag(favorite.targetType));
+            } else {
+                // Let LauncherModel know we've been here.
+                values.put(LauncherSettings.Favorites.RESTORED, 1);
+            }
 
             // Verify placement
             if (favorite.container == Favorites.CONTAINER_HOTSEAT) {
@@ -915,7 +977,8 @@
     }
 
     /** Serialize a widget for persistence, including a checksum wrapper. */
-    private Widget packWidget(int dpi, WidgetPreviewLoader previewLoader, IconCache iconCache,
+    private Widget packWidget(int dpi, WidgetPreviewLoader previewLoader,
+            int previewWidth, IconCache iconCache,
             ComponentName provider, UserHandleCompat user) {
         final LauncherAppWidgetProviderInfo info =
                 LauncherModel.getProviderInfo(mContext, provider, user);
@@ -935,7 +998,7 @@
         }
         if (info.previewImage != 0) {
             widget.preview = new Resource();
-            Bitmap preview = previewLoader.generateWidgetPreview(info, null);
+            Bitmap preview = previewLoader.generateWidgetPreview(info, previewWidth, null);
             ByteArrayOutputStream os = new ByteArrayOutputStream();
             if (preview.compress(IMAGE_FORMAT, IMAGE_COMPRESSION_QUALITY, os)) {
                 widget.preview.data = os.toByteArray();
@@ -1094,9 +1157,11 @@
 
         final LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
         if (appState == null) {
-            Throwable stackTrace = new Throwable();
-            stackTrace.fillInStackTrace();
-            Log.w(TAG, "Failed to get app state during backup/restore", stackTrace);
+            if (DEBUG) {
+                Throwable stackTrace = new Throwable();
+                stackTrace.fillInStackTrace();
+                Log.w(TAG, "Failed to get app state during backup/restore", stackTrace);
+            }
             return false;
         }
         mIconCache = appState.getIconCache();
@@ -1131,6 +1196,9 @@
     }
 
     private class InvalidBackupException extends IOException {
+
+        private static final long serialVersionUID = 8931456637211665082L;
+
         private InvalidBackupException(Throwable cause) {
             super(cause);
         }
@@ -1139,4 +1207,54 @@
             super(reason);
         }
     }
+
+    /**
+     * A class to check if an activity can handle one of the intents from a list of
+     * predefined intents.
+     */
+    private class ItemTypeMatcher {
+
+        private final ArrayList<Intent> mIntents;
+
+        ItemTypeMatcher(int xml_res) {
+            mIntents = xml_res == 0 ? new ArrayList<Intent>() : parseIntents(xml_res);
+        }
+
+        private ArrayList<Intent> parseIntents(int xml_res) {
+            ArrayList<Intent> intents = new ArrayList<Intent>();
+            XmlResourceParser parser = mContext.getResources().getXml(xml_res);
+            try {
+                DefaultLayoutParser.beginDocument(parser, DefaultLayoutParser.TAG_RESOLVE);
+                final int depth = parser.getDepth();
+                int type;
+                while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                    if (type != XmlPullParser.START_TAG) {
+                        continue;
+                    } else if (DefaultLayoutParser.TAG_FAVORITE.equals(parser.getName())) {
+                        final String uri = DefaultLayoutParser.getAttributeValue(
+                                parser, DefaultLayoutParser.ATTR_URI);
+                        intents.add(Intent.parseUri(uri, 0));
+                    }
+                }
+            } catch (URISyntaxException | XmlPullParserException | IOException e) {
+                Log.e(TAG, "Unable to parse " + xml_res, e);
+            } finally {
+                parser.close();
+            }
+            return intents;
+        }
+
+        public boolean matches(ActivityInfo activity, PackageManager pm) {
+            for (Intent intent : mIntents) {
+                intent.setPackage(activity.packageName);
+                ResolveInfo info = pm.resolveActivity(intent, 0);
+                if (info != null && (info.activityInfo.name.equals(activity.name)
+                        || info.activityInfo.name.equals(activity.targetActivity))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index d8128d6..2fee81c 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -50,6 +50,7 @@
     public void onLauncherProviderChange();
     public void finishBindingItems(final boolean upgradePath);
     public void onClickAllAppsButton(View v);
+    public void onAllAppsShown();
     public void bindAllApplications(ArrayList<AppInfo> apps);
     public void onClickFolderIcon(View v);
     public void onClickAppShortcut(View v);
@@ -87,6 +88,7 @@
     public ComponentName getWallpaperPickerComponent();
     public boolean overrideWallpaperDimensions();
     public boolean isLauncherPreinstalled();
+    public boolean overrideAllAppsSearch();
 
     /**
      * Returning true will immediately result in a call to {@link #setLauncherOverlayView(ViewGroup,
@@ -106,4 +108,12 @@
     public Launcher.LauncherOverlay setLauncherOverlayView(InsettableFrameLayout container,
             Launcher.LauncherOverlayCallbacks callbacks);
 
+    /**
+     * Sets the callbacks to allow any extensions to callback to the launcher.
+     *
+     * @param callbacks A set of callbacks to the Launcher, is actually a LauncherAppsCallback, but
+     *                  for implementation purposes is passed around as an object.
+     */
+    public void setLauncherAppsCallback(Object callbacks);
+
 }
diff --git a/src/com/android/launcher3/LauncherClings.java b/src/com/android/launcher3/LauncherClings.java
index ef8e8ab..2ce8b1c 100644
--- a/src/com/android/launcher3/LauncherClings.java
+++ b/src/com/android/launcher3/LauncherClings.java
@@ -35,6 +35,8 @@
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.launcher3.util.Thunk;
+
 class LauncherClings implements OnClickListener {
     private static final String MIGRATION_CLING_DISMISSED_KEY = "cling_gel.migration.dismissed";
     private static final String WORKSPACE_CLING_DISMISSED_KEY = "cling_gel.workspace.dismissed";
@@ -49,7 +51,7 @@
     // New Secure Setting in L
     private static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints";
 
-    private Launcher mLauncher;
+    @Thunk Launcher mLauncher;
     private LayoutInflater mInflater;
 
     /** Ctor */
@@ -174,7 +176,7 @@
         });
     }
 
-    private void dismissLongPressCling() {
+    @Thunk void dismissLongPressCling() {
         Runnable dismissCb = new Runnable() {
             public void run() {
                 dismissCling(mLauncher.findViewById(R.id.longpress_cling), null,
@@ -185,7 +187,7 @@
     }
 
     /** Hides the specified Cling */
-    private void dismissCling(final View cling, final Runnable postAnimationCb,
+    @Thunk void dismissCling(final View cling, final Runnable postAnimationCb,
                               final String flag, int duration) {
         // To catch cases where siblings of top-level views are made invisible, just check whether
         // the cling is directly set to GONE before dismissing it.
diff --git a/src/com/android/launcher3/LauncherExtension.java b/src/com/android/launcher3/LauncherExtension.java
index fe9bd6c..e4fdbbc 100644
--- a/src/com/android/launcher3/LauncherExtension.java
+++ b/src/com/android/launcher3/LauncherExtension.java
@@ -124,6 +124,10 @@
         }
 
         @Override
+        public void onAllAppsShown() {
+        }
+
+        @Override
         public void bindAllApplications(ArrayList<AppInfo> apps) {
         }
 
@@ -246,6 +250,11 @@
         }
 
         @Override
+        public boolean overrideAllAppsSearch() {
+            return false;
+        }
+
+        @Override
         public boolean isLauncherPreinstalled() {
             return false;
         }
@@ -265,6 +274,11 @@
             return mLauncherOverlay;
         }
 
+        @Override
+        public void setLauncherAppsCallback(Object callbacks) {
+            // Do nothing
+        }
+
         class LauncherExtensionOverlay implements LauncherOverlay {
             LauncherOverlayCallbacks mLauncherOverlayCallbacks;
             ViewGroup mOverlayView;
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index fa05365..9dd8dc5 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -18,23 +18,29 @@
     public static final String DEFAULT_WALLPAPER_THUMBNAIL_OLD = "default_thumb.jpg";
     public static final String LAUNCHER_DB = "launcher.db";
     public static final String LAUNCHER_PREFERENCES = "launcher.preferences";
-    public static final String LAUNCHES_LOG = "launches.log";
     public static final String SHARED_PREFERENCES_KEY = "com.android.launcher3.prefs";
-    public static final String STATS_LOG = "stats.log";
     public static final String WALLPAPER_CROP_PREFERENCES_KEY =
-            WallpaperCropActivity.class.getName();
+            "com.android.launcher3.WallpaperCropActivity";
+    public static final String MANAGED_USER_PREFERENCES_KEY = "com.android.launcher3.managedusers.prefs";
+
     public static final String WALLPAPER_IMAGES_DB = "saved_wallpaper_images.db";
     public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db";
+    public static final String APP_ICONS_DB = "app_icons.db";
 
     public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList(
             DEFAULT_WALLPAPER_THUMBNAIL,
             DEFAULT_WALLPAPER_THUMBNAIL_OLD,
             LAUNCHER_DB,
             LAUNCHER_PREFERENCES,
-            LAUNCHES_LOG,
             SHARED_PREFERENCES_KEY + XML,
-            STATS_LOG,
             WALLPAPER_CROP_PREFERENCES_KEY + XML,
             WALLPAPER_IMAGES_DB,
-            WIDGET_PREVIEWS_DB));
+            WIDGET_PREVIEWS_DB,
+            MANAGED_USER_PREFERENCES_KEY,
+            APP_ICONS_DB));
+
+    // TODO: Delete these files on upgrade
+    public static final List<String> OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList(
+            "launches.log",
+            "stats.log"));
 }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 2e5d4e9..f7df6bc 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -36,10 +36,8 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.graphics.Rect;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -61,6 +59,8 @@
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ManagedProfileHeuristic;
+import com.android.launcher3.util.Thunk;
 
 import java.lang.ref.WeakReference;
 import java.net.URISyntaxException;
@@ -77,7 +77,6 @@
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.TreeMap;
 
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
@@ -89,13 +88,9 @@
     static final boolean DEBUG_LOADERS = false;
     private static final boolean DEBUG_RECEIVER = false;
     private static final boolean REMOVE_UNRESTORED_ICONS = true;
-    private static final boolean ADD_MANAGED_PROFILE_SHORTCUTS = false;
 
     static final String TAG = "Launcher.Model";
 
-    // true = use a "More Apps" folder for non-workspace apps on upgrade
-    // false = strew non-workspace apps across the workspace on upgrade
-    public static final boolean UPGRADE_USE_MORE_APPS_FOLDER = false;
     public static final int LOADER_FLAG_NONE = 0;
     public static final int LOADER_FLAG_CLEAR_WORKSPACE = 1 << 0;
     public static final int LOADER_FLAG_MIGRATE_SHORTCUTS = 1 << 1;
@@ -103,19 +98,14 @@
     private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons
     private static final long INVALID_SCREEN_ID = -1L;
 
-    private final boolean mAppsCanBeOnRemoveableStorage;
+    @Thunk final boolean mAppsCanBeOnRemoveableStorage;
     private final boolean mOldContentProviderExists;
 
-    private final LauncherAppState mApp;
-    private final Object mLock = new Object();
-    private DeferredHandler mHandler = new DeferredHandler();
-    private LoaderTask mLoaderTask;
-    private boolean mIsLoaderTaskRunning;
-
-    /**
-     * Maintain a set of packages per user, for which we added a shortcut on the workspace.
-     */
-    private static final String INSTALLED_SHORTCUTS_SET_PREFIX = "installed_shortcuts_set_for_user_";
+    @Thunk final LauncherAppState mApp;
+    @Thunk final Object mLock = new Object();
+    @Thunk DeferredHandler mHandler = new DeferredHandler();
+    @Thunk LoaderTask mLoaderTask;
+    @Thunk boolean mIsLoaderTaskRunning;
 
     // Specific runnable types that are run on the main thread deferred handler, this allows us to
     // clear all queued binding runnables when the Launcher activity is destroyed.
@@ -124,17 +114,17 @@
 
     private static final String MIGRATE_AUTHORITY = "com.android.launcher2.settings";
 
-    private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
+    @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
     static {
         sWorkerThread.start();
     }
-    private static final Handler sWorker = new Handler(sWorkerThread.getLooper());
+    @Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper());
 
     // We start off with everything not loaded.  After that, we assume that
     // our monitoring of the package manager provides all updates and we never
     // need to do a requery.  These are only ever touched from the loader thread.
-    private boolean mWorkspaceLoaded;
-    private boolean mAllAppsLoaded;
+    @Thunk boolean mWorkspaceLoaded;
+    @Thunk boolean mAllAppsLoaded;
 
     // When we are loading pages synchronously, we can't just post the binding of items on the side
     // pages as this delays the rotation process.  Instead, we wait for a callback from the first
@@ -142,7 +132,7 @@
     // a normal load, we also clear this set of Runnables.
     static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>();
 
-    private WeakReference<Callbacks> mCallbacks;
+    @Thunk WeakReference<Callbacks> mCallbacks;
 
     // < only access in worker thread >
     AllAppsList mBgAllAppsList;
@@ -169,9 +159,6 @@
     // sBgFolders is all FolderInfos created by LauncherModel. Passed to bindFolders()
     static final HashMap<Long, FolderInfo> sBgFolders = new HashMap<Long, FolderInfo>();
 
-    // sBgDbIconCache is the set of ItemInfos that need to have their icons updated in the database
-    static final HashMap<Object, byte[]> sBgDbIconCache = new HashMap<Object, byte[]>();
-
     // sBgWorkspaceScreens is the ordered set of workspace screens.
     static final ArrayList<Long> sBgWorkspaceScreens = new ArrayList<Long>();
 
@@ -184,12 +171,12 @@
 
     // </ only access in worker thread >
 
-    private IconCache mIconCache;
+    @Thunk IconCache mIconCache;
 
     protected int mPreviousConfigMcc;
 
-    private final LauncherAppsCompat mLauncherApps;
-    private final UserManagerCompat mUserManager;
+    @Thunk final LauncherAppsCompat mLauncherApps;
+    @Thunk final UserManagerCompat mUserManager;
 
     public interface Callbacks {
         public boolean setLoadOnResume();
@@ -200,7 +187,7 @@
         public void bindScreens(ArrayList<Long> orderedScreenIds);
         public void bindAddScreens(ArrayList<Long> orderedScreenIds);
         public void bindFolders(HashMap<Long,FolderInfo> folders);
-        public void finishBindingItems(boolean upgradePath);
+        public void finishBindingItems();
         public void bindAppWidget(LauncherAppWidgetInfo info);
         public void bindAllApplications(ArrayList<AppInfo> apps);
         public void bindAppsAdded(ArrayList<Long> newScreens,
@@ -267,10 +254,10 @@
 
     /** Runs the specified runnable immediately if called from the main thread, otherwise it is
      * posted on the main thread handler. */
-    private void runOnMainThread(Runnable r) {
+    @Thunk void runOnMainThread(Runnable r) {
         runOnMainThread(r, 0);
     }
-    private void runOnMainThread(Runnable r, int type) {
+    @Thunk void runOnMainThread(Runnable r, int type) {
         if (sWorkerThread.getThreadId() == Process.myTid()) {
             // If we are on the worker thread, post onto the main handler
             mHandler.post(r);
@@ -346,9 +333,9 @@
         runOnWorkerThread(r);
     }
 
-    public void addAndBindAddedWorkspaceApps(final Context context,
+    public void addAndBindAddedWorkspaceItems(final Context context,
             final ArrayList<ItemInfo> workspaceApps) {
-        addAndBindAddedWorkspaceApps(context, workspaceApps,
+        addAndBindAddedWorkspaceItems(context, workspaceApps,
                 new ScreenPosProvider() {
 
                     @Override
@@ -381,7 +368,7 @@
      * Find a position on the screen for the given size or adds a new screen.
      * @return screenId and the coordinates for the item.
      */
-    private static Pair<Long, int[]> findSpaceForItem(
+    @Thunk static Pair<Long, int[]> findSpaceForItem(
             Context context,
             ScreenPosProvider preferredScreen,
             int fallbackStartScreen,
@@ -492,13 +479,7 @@
         Runnable r = new Runnable() {
             public void run() {
                 final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();
-
-                ArrayList<Long> workspaceScreens = new ArrayList<Long>();
-                TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(context);
-                for (Integer i : orderedScreens.keySet()) {
-                    long screenId = orderedScreens.get(i);
-                    workspaceScreens.add(screenId);
-                }
+                ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context);
 
                 // Find appropriate space for the item.
                 Pair<Long, int[]> coords = findSpaceForItem(context, preferredScreen,
@@ -532,7 +513,7 @@
      * @param fallbackStartScreen the screen to start search for empty space if
      * preferredScreen is not available.
      */
-    public void addAndBindAddedWorkspaceApps(final Context context,
+    public void addAndBindAddedWorkspaceItems(final Context context,
             final ArrayList<ItemInfo> workspaceApps,
             final ScreenPosProvider preferredScreen,
             final int fallbackStartScreen,
@@ -550,16 +531,10 @@
                 // Get the list of workspace screens.  We need to append to this list and
                 // can not use sBgWorkspaceScreens because loadWorkspace() may not have been
                 // called.
-                ArrayList<Long> workspaceScreens = new ArrayList<Long>();
-                TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(context);
-                for (Integer i : orderedScreens.keySet()) {
-                    long screenId = orderedScreens.get(i);
-                    workspaceScreens.add(screenId);
-                }
-
+                ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context);
                 synchronized(sBgLock) {
                     for (ItemInfo item : workspaceApps) {
-                        if (!allowDuplicate) {
+                        if (!allowDuplicate && item instanceof ShortcutInfo) {
                             // Short-circuit this logic if the icon exists somewhere on the workspace
                             if (shortcutExists(context, item.title.toString(),
                                     item.getIntent(), item.user)) {
@@ -574,21 +549,21 @@
                         long screenId = coords.first;
                         int[] cordinates = coords.second;
 
-                        ShortcutInfo shortcutInfo;
-                        if (item instanceof ShortcutInfo) {
-                            shortcutInfo = (ShortcutInfo) item;
+                        ItemInfo itemInfo;
+                        if (item instanceof ShortcutInfo || item instanceof FolderInfo) {
+                            itemInfo = item;
                         } else if (item instanceof AppInfo) {
-                            shortcutInfo = ((AppInfo) item).makeShortcut();
+                            itemInfo = ((AppInfo) item).makeShortcut();
                         } else {
                             throw new RuntimeException("Unexpected info type");
                         }
 
                         // Add the shortcut to the db
-                        addItemToDatabase(context, shortcutInfo,
+                        addItemToDatabase(context, itemInfo,
                                 LauncherSettings.Favorites.CONTAINER_DESKTOP,
-                                screenId, cordinates[0], cordinates[1], false);
+                                screenId, cordinates[0], cordinates[1]);
                         // Save the ShortcutInfo for binding in the workspace
-                        addedShortcutsFinal.add(shortcutInfo);
+                        addedShortcutsFinal.add(itemInfo);
                     }
                 }
 
@@ -672,7 +647,7 @@
             long screenId, int cellX, int cellY) {
         if (item.container == ItemInfo.NO_ID) {
             // From all apps
-            addItemToDatabase(context, item, container, screenId, cellX, cellY, false);
+            addItemToDatabase(context, item, container, screenId, cellX, cellY);
         } else {
             // From somewhere else
             moveItemInDatabase(context, item, container, screenId, cellX, cellY);
@@ -738,7 +713,7 @@
     static void updateItemInDatabaseHelper(Context context, final ContentValues values,
             final ItemInfo item, final String callingFunction) {
         final long itemId = item.id;
-        final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false);
+        final Uri uri = LauncherSettings.Favorites.getContentUri(itemId);
         final ContentResolver cr = context.getContentResolver();
 
         final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
@@ -764,7 +739,7 @@
                 for (int i = 0; i < count; i++) {
                     ItemInfo item = items.get(i);
                     final long itemId = item.id;
-                    final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false);
+                    final Uri uri = LauncherSettings.Favorites.getContentUri(itemId);
                     ContentValues values = valuesList.get(i);
 
                     ops.add(ContentProviderOperation.newUpdate(uri).withValues(values).build());
@@ -983,6 +958,7 @@
                 final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
                 final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
                 final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
+                final int optionsIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.OPTIONS);
 
                 FolderInfo folderInfo = null;
                 switch (c.getInt(itemTypeIndex)) {
@@ -997,6 +973,7 @@
                 folderInfo.screenId = c.getInt(screenIndex);
                 folderInfo.cellX = c.getInt(cellXIndex);
                 folderInfo.cellY = c.getInt(cellYIndex);
+                folderInfo.options = c.getInt(optionsIndex);
 
                 return folderInfo;
             }
@@ -1011,8 +988,8 @@
      * Add an item to the database in a specified container. Sets the container, screen, cellX and
      * cellY fields of the item. Also assigns an ID to the item.
      */
-    static void addItemToDatabase(Context context, final ItemInfo item, final long container,
-            final long screenId, final int cellX, final int cellY, final boolean notify) {
+    public static void addItemToDatabase(Context context, final ItemInfo item, final long container,
+            final long screenId, final int cellX, final int cellY) {
         item.container = container;
         item.cellX = cellX;
         item.cellY = cellY;
@@ -1035,8 +1012,7 @@
         final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
         Runnable r = new Runnable() {
             public void run() {
-                cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
-                        LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);
+                cr.insert(LauncherSettings.Favorites.CONTENT_URI, values);
 
                 // Lock on mBgLock *after* the db operation
                 synchronized (sBgLock) {
@@ -1103,7 +1079,7 @@
      * @param context
      * @param item
      */
-    static void deleteItemFromDatabase(Context context, final ItemInfo item) {
+    public static void deleteItemFromDatabase(Context context, final ItemInfo item) {
         ArrayList<ItemInfo> items = new ArrayList<ItemInfo>();
         items.add(item);
         deleteItemsFromDatabase(context, items);
@@ -1116,11 +1092,10 @@
      */
     static void deleteItemsFromDatabase(Context context, final ArrayList<? extends ItemInfo> items) {
         final ContentResolver cr = context.getContentResolver();
-
         Runnable r = new Runnable() {
             public void run() {
                 for (ItemInfo item : items) {
-                    final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false);
+                    final Uri uri = LauncherSettings.Favorites.getContentUri(item.id);
                     cr.delete(uri, null, null);
 
                     // Lock on mBgLock *after* the db operation
@@ -1148,7 +1123,6 @@
                                 break;
                         }
                         sBgItemsIdMap.remove(item.id);
-                        sBgDbIconCache.remove(item);
                     }
                 }
             }
@@ -1211,27 +1185,25 @@
     /**
      * Remove the contents of the specified folder from the database
      */
-    static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) {
+    public static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) {
         final ContentResolver cr = context.getContentResolver();
 
         Runnable r = new Runnable() {
             public void run() {
-                cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null);
+                cr.delete(LauncherSettings.Favorites.getContentUri(info.id), null, null);
                 // Lock on mBgLock *after* the db operation
                 synchronized (sBgLock) {
                     sBgItemsIdMap.remove(info.id);
                     sBgFolders.remove(info.id);
-                    sBgDbIconCache.remove(info);
                     sBgWorkspaceItems.remove(info);
                 }
 
-                cr.delete(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION,
+                cr.delete(LauncherSettings.Favorites.CONTENT_URI,
                         LauncherSettings.Favorites.CONTAINER + "=" + info.id, null);
                 // Lock on mBgLock *after* the db operation
                 synchronized (sBgLock) {
                     for (ItemInfo childInfo : info.contents) {
                         sBgItemsIdMap.remove(childInfo.id);
-                        sBgDbIconCache.remove(childInfo);
                     }
                 }
             }
@@ -1444,40 +1416,31 @@
         }
     }
 
-    /** Loads the workspace screens db into a map of Rank -> ScreenId */
-    private static TreeMap<Integer, Long> loadWorkspaceScreensDb(Context context) {
+    /**
+     * Loads the workspace screen ids in an ordered list.
+     */
+    @Thunk static ArrayList<Long> loadWorkspaceScreensDb(Context context) {
         final ContentResolver contentResolver = context.getContentResolver();
         final Uri screensUri = LauncherSettings.WorkspaceScreens.CONTENT_URI;
-        final Cursor sc = contentResolver.query(screensUri, null, null, null, null);
-        TreeMap<Integer, Long> orderedScreens = new TreeMap<Integer, Long>();
 
+        // Get screens ordered by rank.
+        final Cursor sc = contentResolver.query(screensUri, null, null, null,
+                LauncherSettings.WorkspaceScreens.SCREEN_RANK);
+        ArrayList<Long> screenIds = new ArrayList<Long>();
         try {
-            final int idIndex = sc.getColumnIndexOrThrow(
-                    LauncherSettings.WorkspaceScreens._ID);
-            final int rankIndex = sc.getColumnIndexOrThrow(
-                    LauncherSettings.WorkspaceScreens.SCREEN_RANK);
+            final int idIndex = sc.getColumnIndexOrThrow(LauncherSettings.WorkspaceScreens._ID);
             while (sc.moveToNext()) {
                 try {
-                    long screenId = sc.getLong(idIndex);
-                    int rank = sc.getInt(rankIndex);
-                    orderedScreens.put(rank, screenId);
+                    screenIds.add(sc.getLong(idIndex));
                 } catch (Exception e) {
-                    Launcher.addDumpLog(TAG, "Desktop items loading interrupted - invalid screens: " + e, true);
+                    Launcher.addDumpLog(TAG, "Desktop items loading interrupted"
+                            + " - invalid screens: " + e, true);
                 }
             }
         } finally {
             sc.close();
         }
-
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - loadWorkspaceScreensDb()", true);
-        ArrayList<String> orderedScreensPairs= new ArrayList<String>();
-        for (Integer i : orderedScreens.keySet()) {
-            orderedScreensPairs.add("{ " + i + ": " + orderedScreens.get(i) + " }");
-        }
-        Launcher.addDumpLog(TAG, "11683562 -   screens: " +
-                TextUtils.join(", ", orderedScreensPairs), true);
-        return orderedScreens;
+        return screenIds;
     }
 
     public boolean isAllAppsLoaded() {
@@ -1502,17 +1465,14 @@
     private class LoaderTask implements Runnable {
         private Context mContext;
         private boolean mIsLaunching;
-        private boolean mIsLoadingAndBindingWorkspace;
+        @Thunk boolean mIsLoadingAndBindingWorkspace;
         private boolean mStopped;
-        private boolean mLoadAndBindStepFinished;
+        @Thunk boolean mLoadAndBindStepFinished;
         private int mFlags;
 
-        private HashMap<Object, CharSequence> mLabelCache;
-
         LoaderTask(Context context, boolean isLaunching, int flags) {
             mContext = context;
             mIsLaunching = isLaunching;
-            mLabelCache = new HashMap<Object, CharSequence>();
             mFlags = flags;
         }
 
@@ -1524,8 +1484,7 @@
             return mIsLoadingAndBindingWorkspace;
         }
 
-        /** Returns whether this is an upgrade path */
-        private boolean loadAndBindWorkspace() {
+        private void loadAndBindWorkspace() {
             mIsLoadingAndBindingWorkspace = true;
 
             // Load the workspace
@@ -1533,20 +1492,18 @@
                 Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
             }
 
-            boolean isUpgradePath = false;
             if (!mWorkspaceLoaded) {
-                isUpgradePath = loadWorkspace();
+                loadWorkspace();
                 synchronized (LoaderTask.this) {
                     if (mStopped) {
-                        return isUpgradePath;
+                        return;
                     }
                     mWorkspaceLoaded = true;
                 }
             }
 
             // Bind the workspace
-            bindWorkspace(-1, isUpgradePath);
-            return isUpgradePath;
+            bindWorkspace(-1);
         }
 
         private void waitForIdle() {
@@ -1615,15 +1572,13 @@
 
             // Divide the set of loaded items into those that we are binding synchronously, and
             // everything else that is to be bound normally (asynchronously).
-            bindWorkspace(synchronousBindPage, false);
+            bindWorkspace(synchronousBindPage);
             // XXX: For now, continue posting the binding of AllApps as there are other issues that
             //      arise from that.
             onlyBindAllApps();
         }
 
         public void run() {
-            boolean isUpgrade = false;
-
             synchronized (mLock) {
                 mIsLoaderTaskRunning = true;
             }
@@ -1640,7 +1595,7 @@
                             ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND);
                 }
                 if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
-                isUpgrade = loadAndBindWorkspace();
+                loadAndBindWorkspace();
 
                 if (mStopped) {
                     break keep_running;
@@ -1660,29 +1615,15 @@
                 if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
                 loadAndBindAllApps();
 
+                // Remove entries for packages which changed while the launcher was dead.
+                LauncherAppState.getInstance().getWidgetCache().removeObsoletePreviews();
+
                 // Restore the default thread priority after we are done loading items
                 synchronized (mLock) {
                     android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
                 }
             }
 
-            // Update the saved icons if necessary
-            if (DEBUG_LOADERS) Log.d(TAG, "Comparing loaded icons to database icons");
-            synchronized (sBgLock) {
-                for (Object key : sBgDbIconCache.keySet()) {
-                    updateSavedIcon(mContext, (ShortcutInfo) key, sBgDbIconCache.get(key));
-                }
-                sBgDbIconCache.clear();
-            }
-
-            if (LauncherAppState.isDisableAllApps()) {
-                // Ensure that all the applications that are in the system are
-                // represented on the home screen.
-                if (!UPGRADE_USE_MORE_APPS_FOLDER || !isUpgrade) {
-                    verifyApplications();
-                }
-            }
-
             // Clear out this reference, otherwise we end up holding it until all of the
             // callback runnables are done.
             mContext = null;
@@ -1733,28 +1674,6 @@
             }
         }
 
-        private void verifyApplications() {
-            final Context context = mApp.getContext();
-
-            // Cross reference all the applications in our apps list with items in the workspace
-            ArrayList<ItemInfo> tmpInfos;
-            ArrayList<ItemInfo> added = new ArrayList<ItemInfo>();
-            synchronized (sBgLock) {
-                for (AppInfo app : mBgAllAppsList.data) {
-                    tmpInfos = getItemInfoForComponentName(app.componentName, app.user);
-                    if (tmpInfos.isEmpty()) {
-                        // We are missing an application icon, so add this to the workspace
-                        added.add(app);
-                        // This is a rare event, so lets log it
-                        Log.e(TAG, "Missing Application on load: " + app);
-                    }
-                }
-            }
-            if (!added.isEmpty()) {
-                addAndBindAddedWorkspaceApps(context, added);
-            }
-        }
-
         // check & update map of what's occupied; used to discard overlapping/invalid items
         private boolean checkItemPlacement(HashMap<Long, ItemInfo[][]> occupied, ItemInfo item) {
             LauncherAppState app = LauncherAppState.getInstance();
@@ -1852,13 +1771,11 @@
                 sBgAppWidgets.clear();
                 sBgFolders.clear();
                 sBgItemsIdMap.clear();
-                sBgDbIconCache.clear();
                 sBgWorkspaceScreens.clear();
             }
         }
 
-        /** Returns whether this is an upgrade path */
-        private boolean loadWorkspace() {
+        private void loadWorkspace() {
             // Log to disk
             Launcher.addDumpLog(TAG, "11683562 - loadWorkspace()", true);
 
@@ -1892,12 +1809,6 @@
                 LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary();
             }
 
-            // This code path is for our old migration code and should no longer be exercised
-            boolean loadedOldDb = false;
-
-            // Log to disk
-            Launcher.addDumpLog(TAG, "11683562 -   loadedOldDb: " + loadedOldDb, true);
-
             synchronized (sBgLock) {
                 clearSBgDataStructures();
                 final HashSet<String> installingPkgs = PackageInstallerCompat
@@ -1905,7 +1816,7 @@
 
                 final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
                 final ArrayList<Long> restoredRows = new ArrayList<Long>();
-                final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION;
+                final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
                 if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
                 final Cursor c = contentResolver.query(contentUri, null, null, null, null);
 
@@ -1951,9 +1862,8 @@
                             LauncherSettings.Favorites.RESTORED);
                     final int profileIdIndex = c.getColumnIndexOrThrow(
                             LauncherSettings.Favorites.PROFILE_ID);
-                    //final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
-                    //final int displayModeIndex = c.getColumnIndexOrThrow(
-                    //        LauncherSettings.Favorites.DISPLAY_MODE);
+                    final int optionsIndex = c.getColumnIndexOrThrow(
+                            LauncherSettings.Favorites.OPTIONS);
 
                     ShortcutInfo info;
                     String intentDescription;
@@ -1979,6 +1889,7 @@
                                 user = mUserManager.getUserForSerialNumber(serialNumber);
                                 int promiseType = c.getInt(restoredIndex);
                                 int disabledState = 0;
+                                boolean itemReplaced = false;
                                 if (user == null) {
                                     // User has been deleted remove the item.
                                     itemsToRemove.add(id);
@@ -2010,9 +1921,7 @@
                                                     ContentValues values = new ContentValues();
                                                     values.put(LauncherSettings.Favorites.INTENT,
                                                             intent.toUri(0));
-                                                    String where = BaseColumns._ID + "= ?";
-                                                    String[] args = {Long.toString(id)};
-                                                    contentResolver.update(contentUri, values, where, args);
+                                                    updateItem(id, values);
                                                 }
                                             }
 
@@ -2042,10 +1951,27 @@
                                                 ContentValues values = new ContentValues();
                                                 values.put(LauncherSettings.Favorites.RESTORED,
                                                         promiseType);
-                                                String where = BaseColumns._ID + "= ?";
-                                                String[] args = {Long.toString(id)};
-                                                contentResolver.update(contentUri, values, where, args);
+                                                updateItem(id, values);
+                                            } else if ((promiseType & ShortcutInfo.FLAG_RESTORED_APP_TYPE) != 0) {
+                                                // This is a common app. Try to replace this.
+                                                int appType = CommonAppTypeParser.decodeItemTypeFromFlag(promiseType);
+                                                CommonAppTypeParser parser = new CommonAppTypeParser(id, appType, context);
+                                                if (parser.findDefaultApp()) {
+                                                    // Default app found. Replace it.
+                                                    intent = parser.parsedIntent;
+                                                    cn = intent.getComponent();
+                                                    ContentValues values = parser.parsedValues;
+                                                    values.put(LauncherSettings.Favorites.RESTORED, 0);
+                                                    updateItem(id, values);
+                                                    restored = false;
+                                                    itemReplaced = true;
 
+                                                } else if (REMOVE_UNRESTORED_ICONS) {
+                                                    Launcher.addDumpLog(TAG,
+                                                            "Unrestored package removed: " + cn, true);
+                                                    itemsToRemove.add(id);
+                                                    continue;
+                                                }
                                             } else if (REMOVE_UNRESTORED_ICONS) {
                                                 Launcher.addDumpLog(TAG,
                                                         "Unrestored package removed: " + cn, true);
@@ -2091,12 +2017,26 @@
                                     continue;
                                 }
 
-                                if (restored) {
+                                container = c.getInt(containerIndex);
+                                boolean useLowResIcon = container >= 0 &&
+                                        c.getInt(rankIndex) >= FolderIcon.NUM_ITEMS_IN_PREVIEW;
+
+                                if (itemReplaced) {
+                                    if (user.equals(UserHandleCompat.myUserHandle())) {
+                                        info = getAppShortcutInfo(manager, intent, user, context, null,
+                                                iconIndex, titleIndex, false, useLowResIcon);
+                                    } else {
+                                        // Don't replace items for other profiles.
+                                        itemsToRemove.add(id);
+                                        continue;
+                                    }
+                                } else if (restored) {
                                     if (user.equals(UserHandleCompat.myUserHandle())) {
                                         Launcher.addDumpLog(TAG,
                                                 "constructing info for partially restored package",
                                                 true);
-                                        info = getRestoredItemInfo(c, titleIndex, intent, promiseType);
+                                        info = getRestoredItemInfo(c, titleIndex, intent,
+                                                promiseType, useLowResIcon);
                                         intent = getRestoredItemIntent(c, context, intent);
                                     } else {
                                         // Don't restore items for other profiles.
@@ -2105,8 +2045,8 @@
                                     }
                                 } else if (itemType ==
                                         LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
-                                    info = getShortcutInfo(manager, intent, user, context, c,
-                                            iconIndex, titleIndex, mLabelCache, allowMissingTarget);
+                                    info = getAppShortcutInfo(manager, intent, user, context, c,
+                                            iconIndex, titleIndex, allowMissingTarget, useLowResIcon);
                                 } else {
                                     info = getShortcutInfo(c, context, iconTypeIndex,
                                             iconPackageIndex, iconResourceIndex, iconIndex,
@@ -2128,7 +2068,6 @@
                                 if (info != null) {
                                     info.id = id;
                                     info.intent = intent;
-                                    container = c.getInt(containerIndex);
                                     info.container = container;
                                     info.screenId = c.getInt(screenIndex);
                                     info.cellX = c.getInt(cellXIndex);
@@ -2161,10 +2100,6 @@
                                         break;
                                     }
                                     sBgItemsIdMap.put(info.id, info);
-
-                                    // now that we've loaded everthing re-save it with the
-                                    // icon in case it disappears somehow.
-                                    queueIconToBeChecked(sBgDbIconCache, info, c, iconIndex);
                                 } else {
                                     throw new RuntimeException("Unexpected null ShortcutInfo");
                                 }
@@ -2183,6 +2118,7 @@
                                 folderInfo.cellY = c.getInt(cellYIndex);
                                 folderInfo.spanX = 1;
                                 folderInfo.spanY = 1;
+                                folderInfo.options = c.getInt(optionsIndex);
 
                                 // check & update map of what's occupied
                                 if (!checkItemPlacement(occupied, folderInfo)) {
@@ -2327,9 +2263,7 @@
                                                     providerName);
                                             values.put(LauncherSettings.Favorites.RESTORED,
                                                     appWidgetInfo.restoreStatus);
-                                            String where = BaseColumns._ID + "= ?";
-                                            String[] args = {Long.toString(id)};
-                                            contentResolver.update(contentUri, values, where, args);
+                                            updateItem(id, values);
                                         }
                                     }
                                     sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo);
@@ -2350,7 +2284,7 @@
                 // Break early if we've stopped loading
                 if (mStopped) {
                     clearSBgDataStructures();
-                    return false;
+                    return;
                 }
 
                 if (itemsToRemove.size() > 0) {
@@ -2363,8 +2297,7 @@
                         }
                         // Don't notify content observers
                         try {
-                            client.delete(LauncherSettings.Favorites.getContentUri(id, false),
-                                    null, null);
+                            client.delete(LauncherSettings.Favorites.getContentUri(id), null, null);
                         } catch (RemoteException e) {
                             Log.w(TAG, "Could not remove id = " + id);
                         }
@@ -2383,7 +2316,7 @@
                         selectionBuilder.append(")");
                         ContentValues values = new ContentValues();
                         values.put(LauncherSettings.Favorites.RESTORED, 0);
-                        updater.update(LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION,
+                        updater.update(LauncherSettings.Favorites.CONTENT_URI,
                                 values, selectionBuilder.toString(), null);
                     } catch (RemoteException e) {
                         Log.w(TAG, "Could not update restored rows");
@@ -2396,63 +2329,29 @@
                             null, sWorker);
                 }
 
-                if (loadedOldDb) {
-                    long maxScreenId = 0;
-                    // If we're importing we use the old screen order.
-                    for (ItemInfo item: sBgItemsIdMap.values()) {
-                        long screenId = item.screenId;
-                        if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
-                                !sBgWorkspaceScreens.contains(screenId)) {
-                            sBgWorkspaceScreens.add(screenId);
-                            if (screenId > maxScreenId) {
-                                maxScreenId = screenId;
-                            }
-                        }
-                    }
-                    Collections.sort(sBgWorkspaceScreens);
-                    // Log to disk
-                    Launcher.addDumpLog(TAG, "11683562 -   maxScreenId: " + maxScreenId, true);
-                    Launcher.addDumpLog(TAG, "11683562 -   sBgWorkspaceScreens: " +
-                            TextUtils.join(", ", sBgWorkspaceScreens), true);
+                sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext));
+                // Log to disk
+                Launcher.addDumpLog(TAG, "11683562 -   sBgWorkspaceScreens: " +
+                        TextUtils.join(", ", sBgWorkspaceScreens), true);
 
-                    LauncherAppState.getLauncherProvider().updateMaxScreenId(maxScreenId);
+                // Remove any empty screens
+                ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens);
+                for (ItemInfo item: sBgItemsIdMap.values()) {
+                    long screenId = item.screenId;
+                    if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
+                            unusedScreens.contains(screenId)) {
+                        unusedScreens.remove(screenId);
+                    }
+                }
+
+                // If there are any empty screens remove them, and update.
+                if (unusedScreens.size() != 0) {
+                    // Log to disk
+                    Launcher.addDumpLog(TAG, "11683562 -   unusedScreens (to be removed): " +
+                            TextUtils.join(", ", unusedScreens), true);
+
+                    sBgWorkspaceScreens.removeAll(unusedScreens);
                     updateWorkspaceScreenOrder(context, sBgWorkspaceScreens);
-
-                    // Update the max item id after we load an old db
-                    long maxItemId = 0;
-                    // If we're importing we use the old screen order.
-                    for (ItemInfo item: sBgItemsIdMap.values()) {
-                        maxItemId = Math.max(maxItemId, item.id);
-                    }
-                    LauncherAppState.getLauncherProvider().updateMaxItemId(maxItemId);
-                } else {
-                    TreeMap<Integer, Long> orderedScreens = loadWorkspaceScreensDb(mContext);
-                    for (Integer i : orderedScreens.keySet()) {
-                        sBgWorkspaceScreens.add(orderedScreens.get(i));
-                    }
-                    // Log to disk
-                    Launcher.addDumpLog(TAG, "11683562 -   sBgWorkspaceScreens: " +
-                            TextUtils.join(", ", sBgWorkspaceScreens), true);
-
-                    // Remove any empty screens
-                    ArrayList<Long> unusedScreens = new ArrayList<Long>(sBgWorkspaceScreens);
-                    for (ItemInfo item: sBgItemsIdMap.values()) {
-                        long screenId = item.screenId;
-                        if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
-                                unusedScreens.contains(screenId)) {
-                            unusedScreens.remove(screenId);
-                        }
-                    }
-
-                    // If there are any empty screens remove them, and update.
-                    if (unusedScreens.size() != 0) {
-                        // Log to disk
-                        Launcher.addDumpLog(TAG, "11683562 -   unusedScreens (to be removed): " +
-                                TextUtils.join(", ", unusedScreens), true);
-
-                        sBgWorkspaceScreens.removeAll(unusedScreens);
-                        updateWorkspaceScreenOrder(context, sBgWorkspaceScreens);
-                    }
                 }
 
                 if (DEBUG_LOADERS) {
@@ -2481,7 +2380,17 @@
                     }
                 }
             }
-            return loadedOldDb;
+        }
+
+        /**
+         * Partially updates the item without any notification. Must be called on the worker thread.
+         */
+        private void updateItem(long itemId, ContentValues update) {
+            mContext.getContentResolver().update(
+                    LauncherSettings.Favorites.CONTENT_URI,
+                    update,
+                    BaseColumns._ID + "= ?",
+                    new String[]{Long.toString(itemId)});
         }
 
         /** Filters the set of items who are directly or indirectly (via another container) on the
@@ -2678,7 +2587,7 @@
         /**
          * Binds all loaded data to actual views on the main thread.
          */
-        private void bindWorkspace(int synchronizeBindPage, final boolean isUpgradePath) {
+        private void bindWorkspace(int synchronizeBindPage) {
             final long t = SystemClock.uptimeMillis();
             Runnable r;
 
@@ -2782,7 +2691,7 @@
                 public void run() {
                     Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                     if (callbacks != null) {
-                        callbacks.finishBindingItems(isUpgradePath);
+                        callbacks.finishBindingItems();
                     }
 
                     // If we're profiling, ensure this is the last thing in the queue.
@@ -2886,39 +2795,55 @@
                 if (apps == null || apps.isEmpty()) {
                     return;
                 }
-                // Sort the applications by name
-                final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
-                Collections.sort(apps,
-                        new LauncherModel.ShortcutNameComparator(mLabelCache));
-                if (DEBUG_LOADERS) {
-                    Log.d(TAG, "sort took "
-                            + (SystemClock.uptimeMillis()-sortTime) + "ms");
+
+                // Update icon cache
+                HashSet<String> updatedPackages = mIconCache.updateDBIcons(user, apps);
+
+                // If any package icon has changed (app was updated while launcher was dead),
+                // update the corresponding shortcuts.
+                if (!updatedPackages.isEmpty()) {
+                    final ArrayList<ShortcutInfo> updates = new ArrayList<ShortcutInfo>();
+                    synchronized (sBgLock) {
+                        for (ItemInfo info : sBgItemsIdMap.values()) {
+                            if (info instanceof ShortcutInfo && user.equals(info.user)
+                                    && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
+                                ShortcutInfo si = (ShortcutInfo) info;
+                                ComponentName cn = si.getTargetComponent();
+                                if (cn != null && updatedPackages.contains(cn.getPackageName())) {
+                                    si.updateIcon(mIconCache);
+                                    updates.add(si);
+                                }
+                            }
+                        }
+                    }
+
+                    if (!updates.isEmpty()) {
+                        final UserHandleCompat userFinal = user;
+                        mHandler.post(new Runnable() {
+
+                            public void run() {
+                                Callbacks cb = getCallback();
+                                if (cb != null) {
+                                    cb.bindShortcutsChanged(
+                                            updates, new ArrayList<ShortcutInfo>(), userFinal);
+                                }
+                            }
+                        });
+                    }
                 }
 
                 // Create the ApplicationInfos
                 for (int i = 0; i < apps.size(); i++) {
                     LauncherActivityInfoCompat app = apps.get(i);
                     // This builds the icon bitmaps.
-                    mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, mLabelCache));
+                    mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache));
                 }
 
-                if (ADD_MANAGED_PROFILE_SHORTCUTS && !user.equals(UserHandleCompat.myUserHandle())) {
-                    // Add shortcuts for packages which were installed while launcher was dead.
-                    String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX
-                            + mUserManager.getSerialNumberForUser(user);
-                    Set<String> packagesAdded = prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET);
-                    HashSet<String> newPackageSet = new HashSet<String>();
-
-                    for (LauncherActivityInfoCompat info : apps) {
-                        String packageName = info.getComponentName().getPackageName();
-                        if (!packagesAdded.contains(packageName)
-                                && !newPackageSet.contains(packageName)) {
-                            InstallShortcutReceiver.queueInstallShortcut(info, mContext);
-                        }
-                        newPackageSet.add(packageName);
+                if (!user.equals(UserHandleCompat.myUserHandle())) {
+                    ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user);
+                    if (heuristic != null) {
+                        heuristic.processUserApps(apps);
                     }
-
-                    prefs.edit().putStringSet(shortcutsSetKey, newPackageSet).commit();
                 }
             }
             // Huh? Shouldn't this be inside the Runnable below?
@@ -2941,6 +2866,8 @@
                     }
                 }
             });
+            // Cleanup any data stored for a deleted user.
+            ManagedProfileHeuristic.processAllUsers(profiles, mContext);
 
             if (DEBUG_LOADERS) {
                 Log.d(TAG, "Icons processed in "
@@ -2963,7 +2890,7 @@
         sWorker.post(task);
     }
 
-    private class AppsAvailabilityCheck extends BroadcastReceiver {
+    @Thunk class AppsAvailabilityCheck extends BroadcastReceiver {
 
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -3028,68 +2955,43 @@
             final String[] packages = mPackages;
             final int N = packages.length;
             switch (mOp) {
-                case OP_ADD:
+                case OP_ADD: {
                     for (int i=0; i<N; i++) {
                         if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
-                        mIconCache.remove(packages[i], mUser);
+                        mIconCache.updateIconsForPkg(packages[i], mUser);
                         mBgAllAppsList.addPackage(context, packages[i], mUser);
                     }
 
-                    // Auto add shortcuts for added packages.
-                    if (ADD_MANAGED_PROFILE_SHORTCUTS
-                            && !UserHandleCompat.myUserHandle().equals(mUser)) {
-                        SharedPreferences prefs = context.getSharedPreferences(
-                                LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
-                        String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX
-                                + mUserManager.getSerialNumberForUser(mUser);
-                        Set<String> shortcutSet = new HashSet<String>(
-                                prefs.getStringSet(shortcutsSetKey,Collections.EMPTY_SET));
-
-                        for (int i=0; i<N; i++) {
-                            if (!shortcutSet.contains(packages[i])) {
-                                shortcutSet.add(packages[i]);
-                                List<LauncherActivityInfoCompat> activities =
-                                        mLauncherApps.getActivityList(packages[i], mUser);
-                                if (activities != null && !activities.isEmpty()) {
-                                    InstallShortcutReceiver.queueInstallShortcut(
-                                            activities.get(0), context);
-                                }
-                            }
-                        }
-
-                        prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit();
+                    ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
+                    if (heuristic != null) {
+                        heuristic.processPackageAdd(mPackages);
                     }
                     break;
+                }
                 case OP_UPDATE:
                     for (int i=0; i<N; i++) {
                         if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
+                        mIconCache.updateIconsForPkg(packages[i], mUser);
                         mBgAllAppsList.updatePackage(context, packages[i], mUser);
-                        WidgetPreviewLoader.removePackageFromDb(
-                                mApp.getWidgetPreviewCacheDb(), packages[i]);
+                        mApp.getWidgetCache().removePackage(packages[i], mUser);
                     }
                     break;
-                case OP_REMOVE:
-                    // Remove the packageName for the set of auto-installed shortcuts. This
-                    // will ensure that the shortcut when the app is installed again.
-                    if (ADD_MANAGED_PROFILE_SHORTCUTS
-                            && !UserHandleCompat.myUserHandle().equals(mUser)) {
-                        SharedPreferences prefs = context.getSharedPreferences(
-                                LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
-                        String shortcutsSetKey = INSTALLED_SHORTCUTS_SET_PREFIX
-                                + mUserManager.getSerialNumberForUser(mUser);
-                        HashSet<String> shortcutSet = new HashSet<String>(
-                                prefs.getStringSet(shortcutsSetKey, Collections.EMPTY_SET));
-                        shortcutSet.removeAll(Arrays.asList(mPackages));
-                        prefs.edit().putStringSet(shortcutsSetKey, shortcutSet).commit();
+                case OP_REMOVE: {
+                    ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(context, mUser);
+                    if (heuristic != null) {
+                        heuristic.processPackageRemoved(mPackages);
                     }
-                    // Fall through
-                case OP_UNAVAILABLE:
-                    boolean clearCache = mOp == OP_REMOVE;
                     for (int i=0; i<N; i++) {
                         if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
-                        mBgAllAppsList.removePackage(packages[i], mUser, clearCache);
-                        WidgetPreviewLoader.removePackageFromDb(
-                                mApp.getWidgetPreviewCacheDb(), packages[i]);
+                        mIconCache.removeIconsForPkg(packages[i], mUser);
+                    }
+                    // Fall through
+                }
+                case OP_UNAVAILABLE:
+                    for (int i=0; i<N; i++) {
+                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
+                        mBgAllAppsList.removePackage(packages[i], mUser);
+                        mApp.getWidgetCache().removePackage(packages[i], mUser);
                     }
                     break;
             }
@@ -3121,13 +3023,7 @@
                     new HashMap<ComponentName, AppInfo>();
 
             if (added != null) {
-                // Ensure that we add all the workspace applications to the db
-                if (LauncherAppState.isDisableAllApps()) {
-                    final ArrayList<ItemInfo> addedInfos = new ArrayList<ItemInfo>(added);
-                    addAndBindAddedWorkspaceApps(context, addedInfos);
-                } else {
-                    addAppsToAllApps(context, added);
-                }
+                addAppsToAllApps(context, added);
                 for (AppInfo ai : added) {
                     addedOrUpdatedApps.put(ai.componentName, ai);
                 }
@@ -3180,7 +3076,6 @@
                                 AppInfo appInfo = addedOrUpdatedApps.get(cn);
 
                                 if (si.isPromise()) {
-                                    mIconCache.deletePreloadedIcon(cn, mUser);
                                     if (si.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) {
                                         // Auto install icon
                                         PackageManager pm = context.getPackageManager();
@@ -3211,6 +3106,9 @@
                                     si.status &= ~ShortcutInfo.FLAG_RESTORED_ICON
                                             & ~ShortcutInfo.FLAG_AUTOINTALL_ICON
                                             & ~ShortcutInfo.FLAG_INSTALL_SESSION_ACTIVE;
+                                    if (appInfo != null) {
+                                        si.flags = appInfo.flags;
+                                    }
 
                                     infoUpdated = true;
                                     si.updateIcon(mIconCache);
@@ -3321,9 +3219,19 @@
                     }
                 });
             }
-            if (Build.VERSION.SDK_INT < 17) {
-                loadAndBindWidgetsAndShortcuts(context, callbacks);
-            }
+
+            final ArrayList<Object> widgetsAndShortcuts =
+                    getSortedWidgetsAndShortcuts(context, true /* refresh */);
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    Callbacks cb = getCallback();
+                    if (callbacks == cb && cb != null) {
+                        callbacks.bindPackagesUpdated(widgetsAndShortcuts);
+                    }
+                }
+            });
+
             // Write all the logs to disk
             mHandler.post(new Runnable() {
                 public void run() {
@@ -3372,25 +3280,6 @@
         }
     }
 
-    public void loadAndBindWidgetsAndShortcuts(final Context context, final Callbacks callbacks) {
-        runOnWorkerThread(new Runnable(){
-            @Override
-            public void run() {
-                final ArrayList<Object> list =
-                        getSortedWidgetsAndShortcuts(context, true /* refresh */);
-                mHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        Callbacks cb = getCallback();
-                        if (callbacks == cb && cb != null) {
-                            callbacks.bindPackagesUpdated(list);
-                        }
-                    }
-                });
-            }
-        });
-    }
-
     // Returns a list of ResolveInfos/AppWidgetInfos in sorted order
     public static ArrayList<Object> getSortedWidgetsAndShortcuts(Context context, boolean refresh) {
         PackageManager packageManager = context.getPackageManager();
@@ -3402,7 +3291,7 @@
         return widgetsAndShortcuts;
     }
 
-    private static boolean isPackageDisabled(Context context, String packageName,
+    @Thunk static boolean isPackageDisabled(Context context, String packageName,
             UserHandleCompat user) {
         final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
         return !launcherApps.isPackageEnabledForProfile(packageName, user);
@@ -3434,10 +3323,10 @@
      * to a package that is not yet installed on the system.
      */
     public ShortcutInfo getRestoredItemInfo(Cursor cursor, int titleIndex, Intent intent,
-            int promiseType) {
+            int promiseType, boolean useLowResIcon) {
         final ShortcutInfo info = new ShortcutInfo();
         info.user = UserHandleCompat.myUserHandle();
-        mIconCache.getTitleAndIcon(info, intent, info.user, true);
+        mIconCache.getTitleAndIcon(info, intent, info.user, useLowResIcon);
 
         if ((promiseType & ShortcutInfo.FLAG_RESTORED_ICON) != 0) {
             String title = (cursor != null) ? cursor.getString(titleIndex) : null;
@@ -3465,7 +3354,7 @@
      * Make an Intent object for a restored application or shortcut item that points
      * to the market page for the item.
      */
-    private Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) {
+    @Thunk Intent getRestoredItemIntent(Cursor c, Context context, Intent intent) {
         ComponentName componentName = intent.getComponent();
         return getMarketIntent(componentName.getPackageName());
     }
@@ -3480,22 +3369,13 @@
     }
 
     /**
-     * This is called from the code that adds shortcuts from the intent receiver.  This
-     * doesn't have a Cursor, but
-     */
-    public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent,
-            UserHandleCompat user, Context context) {
-        return getShortcutInfo(manager, intent, user, context, null, -1, -1, null, false);
-    }
-
-    /**
      * Make an ShortcutInfo object for a shortcut that is an application.
      *
      * If c is not null, then it will be used to fill in missing data like the title and icon.
      */
-    public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent,
+    public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent,
             UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex,
-            HashMap<Object, CharSequence> labelCache, boolean allowMissingTarget) {
+            boolean allowMissingTarget, boolean useLowResIcon) {
         if (user == null) {
             Log.d(TAG, "Null user found in getShortcutInfo");
             return null;
@@ -3517,52 +3397,29 @@
         }
 
         final ShortcutInfo info = new ShortcutInfo();
-
-        // the resource -- This may implicitly give us back the fallback icon,
-        // but don't worry about that.  All we're doing with usingFallbackIcon is
-        // to avoid saving lots of copies of that in the database, and most apps
-        // have icons anyway.
-        Bitmap icon = mIconCache.getIcon(componentName, lai, labelCache);
-
-        // the db
-        if (icon == null) {
-            if (c != null) {
-                icon = getIconFromCursor(c, iconIndex, context);
-            }
-        }
-        // the fallback icon
-        if (icon == null) {
-            icon = mIconCache.getDefaultIcon(user);
-            info.usingFallbackIcon = true;
-        }
-        info.setIcon(icon);
-
-        // From the cache.
-        if (labelCache != null) {
-            info.title = labelCache.get(componentName);
+        mIconCache.getTitleAndIcon(info, componentName, lai, user, false, useLowResIcon);
+        if (mIconCache.isDefaultIcon(info.getIcon(mIconCache), user) && c != null) {
+            Bitmap icon = Utilities.createIconBitmap(c, iconIndex, context);
+            info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon);
         }
 
-        // from the resource
-        if (info.title == null && lai != null) {
-            info.title = lai.getLabel();
-            if (labelCache != null) {
-                labelCache.put(componentName, info.title);
-            }
-        }
         // from the db
-        if (info.title == null) {
-            if (c != null) {
-                info.title =  c.getString(titleIndex);
-            }
+        if (TextUtils.isEmpty(info.title) && c != null) {
+            info.title =  c.getString(titleIndex);
         }
+
         // fall back to the class name of the activity
         if (info.title == null) {
             info.title = componentName.getClassName();
         }
+
         info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
         info.user = user;
         info.contentDescription = mUserManager.getBadgedLabelForUser(
                 info.title.toString(), info.user);
+        if (lai != null) {
+            info.flags = AppInfo.initFlags(lai);
+        }
         return info;
     }
 
@@ -3595,7 +3452,7 @@
         return new ArrayList<ItemInfo>(filtered);
     }
 
-    private ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname,
+    @Thunk ArrayList<ItemInfo> getItemInfoForComponentName(final ComponentName cname,
             final UserHandleCompat user) {
         ItemInfoFilter filter  = new ItemInfoFilter() {
             @Override
@@ -3613,7 +3470,7 @@
     /**
      * Make an ShortcutInfo object for a shortcut that isn't an application.
      */
-    private ShortcutInfo getShortcutInfo(Cursor c, Context context,
+    @Thunk ShortcutInfo getShortcutInfo(Cursor c, Context context,
             int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex,
             int titleIndex) {
 
@@ -3637,7 +3494,7 @@
             icon = Utilities.createIconBitmap(packageName, resourceName, mIconCache, context);
             // the db
             if (icon == null) {
-                icon = getIconFromCursor(c, iconIndex, context);
+                icon = Utilities.createIconBitmap(c, iconIndex, context);
             }
             // the fallback icon
             if (icon == null) {
@@ -3646,7 +3503,7 @@
             }
             break;
         case LauncherSettings.Favorites.ICON_TYPE_BITMAP:
-            icon = getIconFromCursor(c, iconIndex, context);
+            icon = Utilities.createIconBitmap(c, iconIndex, context);
             if (icon == null) {
                 icon = mIconCache.getDefaultIcon(info.user);
                 info.customIcon = false;
@@ -3665,22 +3522,6 @@
         return info;
     }
 
-    Bitmap getIconFromCursor(Cursor c, int iconIndex, Context context) {
-        @SuppressWarnings("all") // suppress dead code warning
-        final boolean debug = false;
-        if (debug) {
-            Log.d(TAG, "getIconFromCursor app="
-                    + c.getString(c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE)));
-        }
-        byte[] data = c.getBlob(iconIndex);
-        try {
-            return Utilities.createIconBitmap(
-                    BitmapFactory.decodeByteArray(data, 0, data.length), context);
-        } catch (Exception e) {
-            return null;
-        }
-    }
-
     ShortcutInfo infoFromShortcutIntent(Context context, Intent data) {
         Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
         String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
@@ -3729,50 +3570,11 @@
         return info;
     }
 
-    boolean queueIconToBeChecked(HashMap<Object, byte[]> cache, ShortcutInfo info, Cursor c,
-            int iconIndex) {
-        // If apps can't be on SD, don't even bother.
-        if (!mAppsCanBeOnRemoveableStorage) {
-            return false;
-        }
-        // If this icon doesn't have a custom icon, check to see
-        // what's stored in the DB, and if it doesn't match what
-        // we're going to show, store what we are going to show back
-        // into the DB.  We do this so when we're loading, if the
-        // package manager can't find an icon (for example because
-        // the app is on SD) then we can use that instead.
-        if (!info.customIcon && !info.usingFallbackIcon) {
-            cache.put(info, c.getBlob(iconIndex));
-            return true;
-        }
-        return false;
-    }
-    void updateSavedIcon(Context context, ShortcutInfo info, byte[] data) {
-        boolean needSave = false;
-        try {
-            if (data != null) {
-                Bitmap saved = BitmapFactory.decodeByteArray(data, 0, data.length);
-                Bitmap loaded = info.getIcon(mIconCache);
-                needSave = !saved.sameAs(loaded);
-            } else {
-                needSave = true;
-            }
-        } catch (Exception e) {
-            needSave = true;
-        }
-        if (needSave) {
-            Log.d(TAG, "going to save icon bitmap for info=" + info);
-            // This is slower than is ideal, but this only happens once
-            // or when the app is updated with a new icon.
-            updateItemInDatabase(context, info);
-        }
-    }
-
     /**
      * Return an existing FolderInfo object if we have encountered this ID previously,
      * or make a new one.
      */
-    private static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) {
+    @Thunk static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) {
         // See if a placeholder was created for us already
         FolderInfo folderInfo = folders.get(id);
         if (folderInfo == null) {
@@ -3783,79 +3585,13 @@
         return folderInfo;
     }
 
-    public static final Comparator<AppInfo> getAppNameComparator() {
-        final Collator collator = Collator.getInstance();
-        return new Comparator<AppInfo>() {
-            public final int compare(AppInfo a, AppInfo b) {
-                if (a.user.equals(b.user)) {
-                    int result = collator.compare(a.title.toString().trim(),
-                            b.title.toString().trim());
-                    if (result == 0) {
-                        result = a.componentName.compareTo(b.componentName);
-                    }
-                    return result;
-                } else {
-                    // TODO Need to figure out rules for sorting
-                    // profiles, this puts work second.
-                    return a.user.toString().compareTo(b.user.toString());
-                }
-            }
-        };
-    }
-    public static final Comparator<AppInfo> APP_INSTALL_TIME_COMPARATOR
-            = new Comparator<AppInfo>() {
-        public final int compare(AppInfo a, AppInfo b) {
-            if (a.firstInstallTime < b.firstInstallTime) return 1;
-            if (a.firstInstallTime > b.firstInstallTime) return -1;
-            return 0;
-        }
-    };
-    static ComponentName getComponentNameFromResolveInfo(ResolveInfo info) {
-        if (info.activityInfo != null) {
-            return new ComponentName(info.activityInfo.packageName, info.activityInfo.name);
-        } else {
-            return new ComponentName(info.serviceInfo.packageName, info.serviceInfo.name);
-        }
-    }
-    public static class ShortcutNameComparator implements Comparator<LauncherActivityInfoCompat> {
-        private Collator mCollator;
-        private HashMap<Object, CharSequence> mLabelCache;
-        ShortcutNameComparator(PackageManager pm) {
-            mLabelCache = new HashMap<Object, CharSequence>();
-            mCollator = Collator.getInstance();
-        }
-        ShortcutNameComparator(HashMap<Object, CharSequence> labelCache) {
-            mLabelCache = labelCache;
-            mCollator = Collator.getInstance();
-        }
-        public final int compare(LauncherActivityInfoCompat a, LauncherActivityInfoCompat b) {
-            String labelA, labelB;
-            ComponentName keyA = a.getComponentName();
-            ComponentName keyB = b.getComponentName();
-            if (mLabelCache.containsKey(keyA)) {
-                labelA = mLabelCache.get(keyA).toString();
-            } else {
-                labelA = a.getLabel().toString().trim();
-
-                mLabelCache.put(keyA, labelA);
-            }
-            if (mLabelCache.containsKey(keyB)) {
-                labelB = mLabelCache.get(keyB).toString();
-            } else {
-                labelB = b.getLabel().toString().trim();
-
-                mLabelCache.put(keyB, labelB);
-            }
-            return mCollator.compare(labelA, labelB);
-        }
-    };
     public static class WidgetAndShortcutNameComparator implements Comparator<Object> {
         private final AppWidgetManagerCompat mManager;
         private final PackageManager mPackageManager;
         private final HashMap<Object, String> mLabelCache;
         private final Collator mCollator;
 
-        WidgetAndShortcutNameComparator(Context context) {
+        public WidgetAndShortcutNameComparator(Context context) {
             mManager = AppWidgetManagerCompat.getInstance(context);
             mPackageManager = context.getPackageManager();
             mLabelCache = new HashMap<Object, String>();
@@ -3904,4 +3640,13 @@
     public Callbacks getCallback() {
         return mCallbacks != null ? mCallbacks.get() : null;
     }
+
+    /**
+     * @return {@link FolderInfo} if its already loaded.
+     */
+    public FolderInfo findFolderById(Long folderId) {
+        synchronized (sBgLock) {
+            return sBgFolders.get(folderId);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 1040b11..f9f5ae1 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.annotation.TargetApi;
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetManager;
 import android.content.ComponentName;
@@ -29,14 +30,19 @@
 import android.content.Intent;
 import android.content.OperationApplicationException;
 import android.content.SharedPreferences;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
 import android.os.StrictMode;
+import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
@@ -46,6 +52,7 @@
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.ProviderConfig;
+import com.android.launcher3.util.Thunk;
 
 import java.io.File;
 import java.net.URISyntaxException;
@@ -57,20 +64,19 @@
     private static final String TAG = "Launcher.LauncherProvider";
     private static final boolean LOGD = false;
 
-    private static final int MIN_DATABASE_VERSION = 12;
-    private static final int DATABASE_VERSION = 22;
+    private static final int DATABASE_VERSION = 24;
 
     static final String OLD_AUTHORITY = "com.android.launcher2.settings";
     static final String AUTHORITY = ProviderConfig.AUTHORITY;
 
-    static final String TABLE_FAVORITES = "favorites";
-    static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
-    static final String PARAMETER_NOTIFY = "notify";
-    static final String UPGRADED_FROM_OLD_DATABASE = "UPGRADED_FROM_OLD_DATABASE";
+    static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME;
+    static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME;
     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
 
     private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd";
 
+    private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
+
     private LauncherProviderChangeListener mListener;
 
     /**
@@ -126,7 +132,7 @@
         return result;
     }
 
-    private static long dbInsertAndCheck(DatabaseHelper helper,
+    @Thunk static long dbInsertAndCheck(DatabaseHelper helper,
             SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
         if (values == null) {
             throw new RuntimeException("Error: attempting to insert null values");
@@ -144,7 +150,8 @@
 
         // In very limited cases, we support system|signature permission apps to add to the db
         String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD);
-        if (externalAdd != null && "true".equals(externalAdd)) {
+        final boolean isExternalAll = externalAdd != null && "true".equals(externalAdd);
+        if (isExternalAll) {
             if (!mOpenHelper.initializeExternalAdd(initialValues)) {
                 return null;
             }
@@ -156,7 +163,14 @@
         if (rowId < 0) return null;
 
         uri = ContentUris.withAppendedId(uri, rowId);
-        sendNotify(uri);
+        notifyListeners();
+
+        if (isExternalAll) {
+            LauncherAppState app = LauncherAppState.getInstanceNoCreate();
+            if (app != null) {
+                app.reloadWorkspace();
+            }
+        }
 
         return uri;
     }
@@ -181,7 +195,7 @@
             db.endTransaction();
         }
 
-        sendNotify(uri);
+        notifyListeners();
         return values.length;
     }
 
@@ -205,7 +219,7 @@
 
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count = db.delete(args.table, args.where, args.args);
-        if (count > 0) sendNotify(uri);
+        if (count > 0) notifyListeners();
 
         return count;
     }
@@ -217,17 +231,12 @@
         addModifiedTime(values);
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         int count = db.update(args.table, values, args.where, args.args);
-        if (count > 0) sendNotify(uri);
+        if (count > 0) notifyListeners();
 
         return count;
     }
 
-    private void sendNotify(Uri uri) {
-        String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
-        if (notify == null || "true".equals(notify)) {
-            getContext().getContentResolver().notifyChange(uri, null);
-        }
-
+    private void notifyListeners() {
         // always notify the backup agent
         LauncherBackupAgentHelper.dataChanged(getContext());
         if (mListener != null) {
@@ -235,7 +244,7 @@
         }
     }
 
-    private static void addModifiedTime(ContentValues values) {
+    @Thunk static void addModifiedTime(ContentValues values) {
         values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
     }
 
@@ -251,12 +260,6 @@
         return mOpenHelper.generateNewScreenId();
     }
 
-    // This is only required one time while loading the workspace during the
-    // upgrade path, and should never be called from anywhere else.
-    public void updateMaxScreenId(long maxScreenId) {
-        mOpenHelper.updateMaxScreenId(maxScreenId);
-    }
-
     /**
      * Clears all the data for a fresh start.
      */
@@ -274,9 +277,10 @@
 
     /**
      * Loads the default workspace based on the following priority scheme:
-     *   1) From a package provided by play store
-     *   2) From a partner configuration APK, already in the system image
-     *   3) The default configuration for the particular device
+     *   1) From the app restrictions
+     *   2) From a package provided by play store
+     *   3) From a partner configuration APK, already in the system image
+     *   4) The default configuration for the particular device
      */
     synchronized public void loadDefaultFavoritesIfNecessary() {
         String spKey = LauncherAppState.getSharedPreferencesKey();
@@ -285,9 +289,11 @@
         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
             Log.d(TAG, "loading default workspace");
 
-            AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(),
-                    mOpenHelper.mAppWidgetHost, mOpenHelper);
-
+            AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction();
+            if (loader == null) {
+                loader = AutoInstallsLayout.get(getContext(),
+                        mOpenHelper.mAppWidgetHost, mOpenHelper);
+            }
             if (loader == null) {
                 final Partner partner = Partner.get(getContext().getPackageManager());
                 if (partner != null && partner.hasDefaultLayout()) {
@@ -321,6 +327,40 @@
         }
     }
 
+    /**
+     * Creates workspace loader from an XML resource listed in the app restrictions.
+     *
+     * @return the loader if the restrictions are set and the resource exists; null otherwise.
+     */
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+    private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() {
+        // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18
+        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            return null;
+        }
+
+        Context ctx = getContext();
+        UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
+        Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
+        if (bundle == null) {
+            return null;
+        }
+
+        String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
+        if (packageName != null) {
+            try {
+                Resources targetResources = ctx.getPackageManager()
+                        .getResourcesForApplication(packageName);
+                return AutoInstallsLayout.get(ctx, packageName, targetResources,
+                        mOpenHelper.mAppWidgetHost, mOpenHelper);
+            } catch (NameNotFoundException e) {
+                Log.e(TAG, "Target package for restricted profile not found", e);
+                return null;
+            }
+        }
+        return null;
+    }
+
     private DefaultLayoutParser getDefaultLayoutParser() {
         int defaultLayout = LauncherAppState.getInstance()
                 .getDynamicGrid().getDeviceProfile().defaultLayoutId;
@@ -350,7 +390,7 @@
 
     private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
         private final Context mContext;
-        private final AppWidgetHost mAppWidgetHost;
+        @Thunk final AppWidgetHost mAppWidgetHost;
         private long mMaxItemId = -1;
         private long mMaxScreenId = -1;
 
@@ -421,7 +461,8 @@
                     "modified INTEGER NOT NULL DEFAULT 0," +
                     "restored INTEGER NOT NULL DEFAULT 0," +
                     "profileId INTEGER DEFAULT " + userSerialNumber + "," +
-                    "rank INTEGER NOT NULL DEFAULT 0" +
+                    "rank INTEGER NOT NULL DEFAULT 0," +
+                    "options INTEGER NOT NULL DEFAULT 0" +
                     ");");
             addWorkspacesTable(db);
 
@@ -478,142 +519,116 @@
         private void setFlagJustLoadedOldDb() {
             String spKey = LauncherAppState.getSharedPreferencesKey();
             SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
-            SharedPreferences.Editor editor = sp.edit();
-            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true);
-            editor.putBoolean(EMPTY_DATABASE_CREATED, false);
-            editor.commit();
+            sp.edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
         }
 
         private void setFlagEmptyDbCreated() {
             String spKey = LauncherAppState.getSharedPreferencesKey();
             SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
-            SharedPreferences.Editor editor = sp.edit();
-            editor.putBoolean(EMPTY_DATABASE_CREATED, true);
-            editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false);
-            editor.commit();
+            sp.edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
         }
 
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
-
-            int version = oldVersion;
-            if (version < MIN_DATABASE_VERSION) {
-                // The version cannot be lower that this, as Launcher3 never supported a lower
+            switch (oldVersion) {
+                // The version cannot be lower that 12, as Launcher3 never supported a lower
                 // version of the DB.
-                createEmptyDB(db);
-                version = DATABASE_VERSION;
-            }
-
-            if (version < 13) {
-                // With the new shrink-wrapped and re-orderable workspaces, it makes sense
-                // to persist workspace screens and their relative order.
-                mMaxScreenId = 0;
-
-                addWorkspacesTable(db);
-                version = 13;
-            }
-
-            if (version < 14) {
-                db.beginTransaction();
-                try {
-                    // Insert new column for holding widget provider name
-                    db.execSQL("ALTER TABLE favorites " +
-                            "ADD COLUMN appWidgetProvider TEXT;");
-                    db.setTransactionSuccessful();
-                    version = 14;
-                } catch (SQLException ex) {
-                    // Old version remains, which means we wipe old data
-                    Log.e(TAG, ex.getMessage(), ex);
-                } finally {
-                    db.endTransaction();
+                case 12: {
+                    // With the new shrink-wrapped and re-orderable workspaces, it makes sense
+                    // to persist workspace screens and their relative order.
+                    mMaxScreenId = 0;
+                    addWorkspacesTable(db);
+                }
+                case 13: {
+                    db.beginTransaction();
+                    try {
+                        // Insert new column for holding widget provider name
+                        db.execSQL("ALTER TABLE favorites " +
+                                "ADD COLUMN appWidgetProvider TEXT;");
+                        db.setTransactionSuccessful();
+                    } catch (SQLException ex) {
+                        Log.e(TAG, ex.getMessage(), ex);
+                        // Old version remains, which means we wipe old data
+                        break;
+                    } finally {
+                        db.endTransaction();
+                    }
+                }
+                case 14: {
+                    db.beginTransaction();
+                    try {
+                        // Insert new column for holding update timestamp
+                        db.execSQL("ALTER TABLE favorites " +
+                                "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
+                        db.execSQL("ALTER TABLE workspaceScreens " +
+                                "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
+                        db.setTransactionSuccessful();
+                    } catch (SQLException ex) {
+                        Log.e(TAG, ex.getMessage(), ex);
+                        // Old version remains, which means we wipe old data
+                        break;
+                    } finally {
+                        db.endTransaction();
+                    }
+                }
+                case 15: {
+                    if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
+                        // Old version remains, which means we wipe old data
+                        break;
+                    }
+                }
+                case 16: {
+                    // We use the db version upgrade here to identify users who may not have seen
+                    // clings yet (because they weren't available), but for whom the clings are now
+                    // available (tablet users). Because one of the possible cling flows (migration)
+                    // is very destructive (wipes out workspaces), we want to prevent this from showing
+                    // until clear data. We do so by marking that the clings have been shown.
+                    LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
+                }
+                case 17: {
+                    // No-op
+                }
+                case 18: {
+                    // Due to a data loss bug, some users may have items associated with screen ids
+                    // which no longer exist. Since this can cause other problems, and since the user
+                    // will never see these items anyway, we use database upgrade as an opportunity to
+                    // clean things up.
+                    removeOrphanedItems(db);
+                }
+                case 19: {
+                    // Add userId column
+                    if (!addProfileColumn(db)) {
+                        // Old version remains, which means we wipe old data
+                        break;
+                    }
+                }
+                case 20:
+                    if (!updateFolderItemsRank(db, true)) {
+                        break;
+                    }
+                case 21:
+                    // Recreate workspace table with screen id a primary key
+                    if (!recreateWorkspaceTable(db)) {
+                        break;
+                    }
+                case 22: {
+                    if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
+                        // Old version remains, which means we wipe old data
+                        break;
+                    }
+                }
+                case 23:
+                    convertShortcutsToLauncherActivities(db);
+                case 24: {
+                    // DB Upgraded successfully
+                    return;
                 }
             }
 
-            if (version < 15) {
-                db.beginTransaction();
-                try {
-                    // Insert new column for holding update timestamp
-                    db.execSQL("ALTER TABLE favorites " +
-                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
-                    db.execSQL("ALTER TABLE workspaceScreens " +
-                            "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
-                    db.setTransactionSuccessful();
-                    version = 15;
-                } catch (SQLException ex) {
-                    // Old version remains, which means we wipe old data
-                    Log.e(TAG, ex.getMessage(), ex);
-                } finally {
-                    db.endTransaction();
-                }
-            }
-
-
-            if (version < 16) {
-                db.beginTransaction();
-                try {
-                    // Insert new column for holding restore status
-                    db.execSQL("ALTER TABLE favorites " +
-                            "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
-                    db.setTransactionSuccessful();
-                    version = 16;
-                } catch (SQLException ex) {
-                    // Old version remains, which means we wipe old data
-                    Log.e(TAG, ex.getMessage(), ex);
-                } finally {
-                    db.endTransaction();
-                }
-            }
-
-            if (version < 17) {
-                // We use the db version upgrade here to identify users who may not have seen
-                // clings yet (because they weren't available), but for whom the clings are now
-                // available (tablet users). Because one of the possible cling flows (migration)
-                // is very destructive (wipes out workspaces), we want to prevent this from showing
-                // until clear data. We do so by marking that the clings have been shown.
-                LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
-                version = 17;
-            }
-
-            if (version < 18) {
-                // No-op
-                version = 18;
-            }
-
-            if (version < 19) {
-                // Due to a data loss bug, some users may have items associated with screen ids
-                // which no longer exist. Since this can cause other problems, and since the user
-                // will never see these items anyway, we use database upgrade as an opportunity to
-                // clean things up.
-                removeOrphanedItems(db);
-                version = 19;
-            }
-
-            if (version < 20) {
-                // Add userId column
-                if (addProfileColumn(db)) {
-                    version = 20;
-                }
-                // else old version remains, which means we wipe old data
-            }
-
-            if (version < 21) {
-                if (updateFolderItemsRank(db, true)) {
-                    version  = 21;
-                }
-            }
-
-            if (version == 21) {
-                // Recreate workspace table with screen id a primary key
-                if (recreateWorkspaceTable(db)) {
-                    version = 22;
-                }
-            }
-
-            if (version != DATABASE_VERSION) {
-                Log.w(TAG, "Destroying all old data.");
-                createEmptyDB(db);
-            }
+            // DB was not upgraded
+            Log.w(TAG, "Destroying all old data.");
+            createEmptyDB(db);
         }
 
         @Override
@@ -624,7 +639,6 @@
             createEmptyDB(db);
         }
 
-
         /**
          * Clears all the data for a fresh start.
          */
@@ -635,6 +649,63 @@
         }
 
         /**
+         * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
+         * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
+         */
+        private void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
+            db.beginTransaction();
+            Cursor c = null;
+            SQLiteStatement updateStmt = null;
+
+            try {
+                // Only consider the primary user as other users can't have a shortcut.
+                long userSerial = UserManagerCompat.getInstance(mContext)
+                        .getSerialNumberForUser(UserHandleCompat.myUserHandle());
+                c = db.query(TABLE_FAVORITES, new String[] {
+                        Favorites._ID,
+                        Favorites.INTENT,
+                    }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
+                    null, null, null, null);
+
+                updateStmt = db.compileStatement("UPDATE favorites SET itemType="
+                        + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?");
+
+                final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
+                final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
+
+                while (c.moveToNext()) {
+                    String intentDescription = c.getString(intentIndex);
+                    Intent intent;
+                    try {
+                        intent = Intent.parseUri(intentDescription, 0);
+                    } catch (URISyntaxException e) {
+                        Log.e(TAG, "Unable to parse intent", e);
+                        continue;
+                    }
+
+                    if (!InstallShortcutReceiver.isLauncherActivity(intent, mContext)) {
+                        continue;
+                    }
+
+                    long id = c.getLong(idIndex);
+                    updateStmt.bindLong(1, id);
+                    updateStmt.execute();
+                }
+                db.setTransactionSuccessful();
+            } catch (SQLException ex) {
+                Log.w(TAG, "Error deduping shortcuts", ex);
+            } finally {
+                db.endTransaction();
+                if (c != null) {
+                    c.close();
+                }
+                if (updateStmt != null) {
+                    updateStmt.close();
+                }
+            }
+        }
+
+        /**
          * Recreates workspace table and migrates data to the new table.
          */
         public boolean recreateWorkspaceTable(SQLiteDatabase db) {
@@ -682,7 +753,7 @@
             return true;
         }
 
-        private boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
+        @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
             db.beginTransaction();
             try {
                 if (addRankColumn) {
@@ -715,20 +786,21 @@
         }
 
         private boolean addProfileColumn(SQLiteDatabase db) {
+            UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
+            // Default to the serial number of this user, for older
+            // shortcuts.
+            long userSerialNumber = userManager.getSerialNumberForUser(
+                    UserHandleCompat.myUserHandle());
+            return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber);
+        }
+
+        private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
             db.beginTransaction();
             try {
-                UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
-                // Default to the serial number of this user, for older
-                // shortcuts.
-                long userSerialNumber = userManager.getSerialNumberForUser(
-                        UserHandleCompat.myUserHandle());
-                // Insert new column for holding user serial number
-                db.execSQL("ALTER TABLE favorites " +
-                        "ADD COLUMN profileId INTEGER DEFAULT "
-                                        + userSerialNumber + ";");
+                db.execSQL("ALTER TABLE favorites ADD COLUMN "
+                        + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
                 db.setTransactionSuccessful();
             } catch (SQLException ex) {
-                // Old version remains, which means we wipe old data
                 Log.e(TAG, ex.getMessage(), ex);
                 return false;
             } finally {
@@ -770,23 +842,7 @@
         }
 
         private long initializeMaxItemId(SQLiteDatabase db) {
-            Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
-
-            // get the result
-            final int maxIdIndex = 0;
-            long id = -1;
-            if (c != null && c.moveToNext()) {
-                id = c.getLong(maxIdIndex);
-            }
-            if (c != null) {
-                c.close();
-            }
-
-            if (id == -1) {
-                throw new RuntimeException("Error: could not query max item id");
-            }
-
-            return id;
+            return getMaxId(db, TABLE_FAVORITES);
         }
 
         // Generates a new ID to use for an workspace screen in your database. This method
@@ -804,35 +860,11 @@
             return mMaxScreenId;
         }
 
-        public void updateMaxScreenId(long maxScreenId) {
-            // Log to disk
-            Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true);
-            mMaxScreenId = maxScreenId;
-        }
-
         private long initializeMaxScreenId(SQLiteDatabase db) {
-            Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
-
-            // get the result
-            final int maxIdIndex = 0;
-            long id = -1;
-            if (c != null && c.moveToNext()) {
-                id = c.getLong(maxIdIndex);
-            }
-            if (c != null) {
-                c.close();
-            }
-
-            if (id == -1) {
-                throw new RuntimeException("Error: could not query max screen id");
-            }
-
-            // Log to disk
-            Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
-            return id;
+            return getMaxId(db, TABLE_WORKSPACE_SCREENS);
         }
 
-        private boolean initializeExternalAdd(ContentValues values) {
+        @Thunk boolean initializeExternalAdd(ContentValues values) {
             // 1. Ensure that externally added items have a valid item id
             long id = generateNewItemId();
             values.put(LauncherSettings.Favorites._ID, id);
@@ -919,7 +951,7 @@
             return rank;
         }
 
-        private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
+        @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
             ArrayList<Long> screenIds = new ArrayList<Long>();
             // TODO: Use multiple loaders with fall-back and transaction.
             int count = loader.loadLayout(db, screenIds);
@@ -946,7 +978,7 @@
             return count;
         }
 
-        private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
+        @Thunk void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
             final ContentResolver resolver = mContext.getContentResolver();
             Cursor c = null;
             int count = 0;
@@ -1242,6 +1274,27 @@
         }
     }
 
+    /**
+     * @return the max _id in the provided table.
+     */
+    @Thunk static long getMaxId(SQLiteDatabase db, String table) {
+        Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null);
+        // get the result
+        long id = -1;
+        if (c != null && c.moveToNext()) {
+            id = c.getLong(0);
+        }
+        if (c != null) {
+            c.close();
+        }
+
+        if (id == -1) {
+            throw new RuntimeException("Error: could not query max id in " + table);
+        }
+
+        return id;
+    }
+
     static class SqlArguments {
         public final String table;
         public final String where;
diff --git a/src/com/android/launcher3/LauncherScroller.java b/src/com/android/launcher3/LauncherScroller.java
index 3bd0a78..a9b4955 100644
--- a/src/com/android/launcher3/LauncherScroller.java
+++ b/src/com/android/launcher3/LauncherScroller.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.hardware.SensorManager;
 import android.os.Build;
-import android.util.FloatMath;
 import android.view.ViewConfiguration;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
@@ -409,7 +408,7 @@
 
             float dx = (float) (mFinalX - mStartX);
             float dy = (float) (mFinalY - mStartY);
-            float hyp = FloatMath.sqrt(dx * dx + dy * dy);
+            float hyp = (float) Math.hypot(dx, dy);
 
             float ndx = dx / hyp;
             float ndy = dy / hyp;
@@ -426,7 +425,7 @@
         mMode = FLING_MODE;
         mFinished = false;
 
-        float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
+        float velocity = (float) Math.hypot(velocityX, velocityY);
 
         mVelocity = velocity;
         mDuration = getSplineFlingDuration(velocity);
diff --git a/src/com/android/launcher3/LauncherSettings.java b/src/com/android/launcher3/LauncherSettings.java
index 13fd7ee..90e60e4 100644
--- a/src/com/android/launcher3/LauncherSettings.java
+++ b/src/com/android/launcher3/LauncherSettings.java
@@ -19,10 +19,12 @@
 import android.net.Uri;
 import android.provider.BaseColumns;
 
+import com.android.launcher3.config.ProviderConfig;
+
 /**
  * Settings related utilities.
  */
-class LauncherSettings {
+public class LauncherSettings {
     /** Columns required on table staht will be subject to backup and restore. */
     static interface ChangeLogColumns extends BaseColumns {
         /**
@@ -45,7 +47,7 @@
          * an Intent that can be launched.
          * <P>Type: TEXT</P>
          */
-        static final String INTENT = "intent";
+        public static final String INTENT = "intent";
 
         /**
          * The type of the gesture
@@ -104,72 +106,59 @@
      *
      * Tracks the order of workspace screens.
      */
-    static final class WorkspaceScreens implements ChangeLogColumns {
+    public static final class WorkspaceScreens implements ChangeLogColumns {
+
+        public static final String TABLE_NAME = "workspaceScreens";
+
         /**
          * The content:// style URL for this table
          */
         static final Uri CONTENT_URI = Uri.parse("content://" +
-                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_WORKSPACE_SCREENS +
-                "?" + LauncherProvider.PARAMETER_NOTIFY + "=true");
+                ProviderConfig.AUTHORITY + "/" + TABLE_NAME);
 
         /**
          * The rank of this screen -- ie. how it is ordered relative to the other screens.
          * <P>Type: INTEGER</P>
          */
-        static final String SCREEN_RANK = "screenRank";
+        public static final String SCREEN_RANK = "screenRank";
     }
 
     /**
      * Favorites.
      */
-    static final class Favorites implements BaseLauncherColumns {
-        /**
-         * The content:// style URL for this table
-         */
-        static final Uri CONTENT_URI = Uri.parse("content://" +
-                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
-                "?" + LauncherProvider.PARAMETER_NOTIFY + "=true");
+    public static final class Favorites implements BaseLauncherColumns {
+
+        public static final String TABLE_NAME = "favorites";
 
         /**
          * The content:// style URL for this table
          */
-        static final Uri OLD_CONTENT_URI = Uri.parse("content://" +
-                LauncherProvider.OLD_AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
-                "?" + LauncherProvider.PARAMETER_NOTIFY + "=true");
-
-        /**
-         * The content:// style URL for this table. When this Uri is used, no notification is
-         * sent if the content changes.
-         */
-        static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" +
-                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
-                "?" + LauncherProvider.PARAMETER_NOTIFY + "=false");
+        public static final Uri CONTENT_URI = Uri.parse("content://" +
+                ProviderConfig.AUTHORITY + "/" + TABLE_NAME);
 
         /**
          * The content:// style URL for a given row, identified by its id.
          *
          * @param id The row id.
-         * @param notify True to send a notification is the content changes.
          *
          * @return The unique content URL for the specified row.
          */
-        static Uri getContentUri(long id, boolean notify) {
-            return Uri.parse("content://" + LauncherProvider.AUTHORITY +
-                    "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" +
-                    LauncherProvider.PARAMETER_NOTIFY + "=" + notify);
+        static Uri getContentUri(long id) {
+            return Uri.parse("content://" + ProviderConfig.AUTHORITY +
+                    "/" + TABLE_NAME + "/" + id);
         }
 
         /**
          * The container holding the favorite
          * <P>Type: INTEGER</P>
          */
-        static final String CONTAINER = "container";
+        public static final String CONTAINER = "container";
 
         /**
          * The icon is a resource identified by a package name and an integer id.
          */
-        static final int CONTAINER_DESKTOP = -100;
-        static final int CONTAINER_HOTSEAT = -101;
+        public static final int CONTAINER_DESKTOP = -100;
+        public static final int CONTAINER_HOTSEAT = -101;
 
         static final String containerToString(int container) {
             switch (container) {
@@ -183,7 +172,7 @@
          * The screen holding the favorite (if container is CONTAINER_DESKTOP)
          * <P>Type: INTEGER</P>
          */
-        static final String SCREEN = "screen";
+        public static final String SCREEN = "screen";
 
         /**
          * The X coordinate of the cell holding the favorite
@@ -236,12 +225,12 @@
         /**
          * The favorite is a widget
          */
-        static final int ITEM_TYPE_APPWIDGET = 4;
+        public static final int ITEM_TYPE_APPWIDGET = 4;
 
         /**
          * The favorite is a custom widget provided by the launcher
          */
-        static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
+        public static final int ITEM_TYPE_CUSTOM_APPWIDGET = 5;
 
         /**
          * The favorite is a clock
@@ -309,5 +298,11 @@
          * <p>Type: INTEGER</p>
          */
         static final String RANK = "rank";
+
+        /**
+         * Stores general flag based options for {@link ItemInfo}s.
+         * <p>Type: INTEGER</p>
+         */
+        static final String OPTIONS = "options";
     }
 }
diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
new file mode 100644
index 0000000..8ba5c60
--- /dev/null
+++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
@@ -0,0 +1,783 @@
+/*
+ * 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.launcher3;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.content.res.Resources;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.WidgetsContainerView;
+
+import java.util.HashMap;
+
+/**
+ * TODO: figure out what kind of tests we can write for this
+ *
+ * Things to test when changing the following class.
+ *   - Home from workspace
+ *          - from center screen
+ *          - from other screens
+ *   - Home from all apps
+ *          - from center screen
+ *          - from other screens
+ *   - Back from all apps
+ *          - from center screen
+ *          - from other screens
+ *   - Launch app from workspace and quit
+ *          - with back
+ *          - with home
+ *   - Launch app from all apps and quit
+ *          - with back
+ *          - with home
+ *   - Go to a screen that's not the default, then all
+ *     apps, and launch and app, and go back
+ *          - with back
+ *          -with home
+ *   - On workspace, long press power and go back
+ *          - with back
+ *          - with home
+ *   - On all apps, long press power and go back
+ *          - with back
+ *          - with home
+ *   - On workspace, power off
+ *   - On all apps, power off
+ *   - Launch an app and turn off the screen while in that app
+ *          - Go back with home key
+ *          - Go back with back key  TODO: make this not go to workspace
+ *          - From all apps
+ *          - From workspace
+ *   - Enter and exit car mode (becuase it causes an extra configuration changed)
+ *          - From all apps
+ *          - From the center workspace
+ *          - From another workspace
+ */
+public class LauncherStateTransitionAnimation {
+
+    /**
+     * Callbacks made during the state transition
+     */
+    interface Callbacks {
+        public void onStateTransitionHideSearchBar();
+    }
+
+    /**
+     * Private callbacks made during transition setup.
+     */
+    static abstract class PrivateTransitionCallbacks {
+        void onRevealViewVisible(View revealView, View contentView, View allAppsButtonView) {}
+        void onAnimationComplete(View revealView, View contentView, View allAppsButtonView) {}
+        float getMaterialRevealViewFinalAlpha(View revealView) {
+            return 0;
+        }
+        float getMaterialRevealViewFinalXDrift(View revealView) {
+            return 0;
+        }
+        float getMaterialRevealViewFinalYDrift(View revealView) {
+            return 0;
+        }
+        float getMaterialRevealViewStartFinalRadius() {
+            return 0;
+        }
+        AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView,
+                View allAppsButtonView) {
+            return null;
+        }
+    }
+
+    public static final String TAG = "LauncherStateTransitionAnimation";
+
+    // Flags to determine how to set the layers on views before the transition animation
+    public static final int BUILD_LAYER = 0;
+    public static final int BUILD_AND_SET_LAYER = 1;
+    public static final int SINGLE_FRAME_DELAY = 16;
+
+    @Thunk Launcher mLauncher;
+    @Thunk Callbacks mCb;
+    @Thunk AnimatorSet mStateAnimation;
+
+    public LauncherStateTransitionAnimation(Launcher l, Callbacks cb) {
+        mLauncher = l;
+        mCb = cb;
+    }
+
+    /**
+     * Starts an animation to the apps view.
+     */
+    public void startAnimationToAllApps(final boolean animated) {
+        final AppsContainerView toView = mLauncher.getAppsView();
+        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
+            private int[] mAllAppsToPanelDelta;
+
+            @Override
+            public void onRevealViewVisible(View revealView, View contentView,
+                    View allAppsButtonView) {
+                toView.setBackground(null);
+                // Get the y delta between the center of the page and the center of the all apps
+                // button
+                mAllAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
+                        allAppsButtonView, null);
+            }
+            @Override
+            public float getMaterialRevealViewFinalAlpha(View revealView) {
+                return 1f;
+            }
+            @Override
+            public float getMaterialRevealViewFinalXDrift(View revealView) {
+                return mAllAppsToPanelDelta[0];
+            }
+            @Override
+            public float getMaterialRevealViewFinalYDrift(View revealView) {
+                return mAllAppsToPanelDelta[1];
+            }
+            @Override
+            public float getMaterialRevealViewStartFinalRadius() {
+                int allAppsButtonSize = LauncherAppState.getInstance().
+                        getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize;
+                return allAppsButtonSize / 2;
+            }
+            @Override
+            public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
+                    final View revealView, final View allAppsButtonView) {
+                return new AnimatorListenerAdapter() {
+                    public void onAnimationStart(Animator animation) {
+                        allAppsButtonView.setVisibility(View.INVISIBLE);
+                    }
+                    public void onAnimationEnd(Animator animation) {
+                        allAppsButtonView.setVisibility(View.VISIBLE);
+                    }
+                };
+            }
+        };
+        startAnimationToOverlay(Workspace.State.NORMAL_HIDDEN, toView, toView.getContentView(),
+                toView.getRevealView(), animated, false /* hideSearchBar */, cb);
+    }
+
+    /**
+     * Starts an animation to the widgets view.
+     */
+    public void startAnimationToWidgets(final boolean animated) {
+        final WidgetsContainerView toView = mLauncher.getWidgetsView();
+        final Resources res = mLauncher.getResources();
+        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
+            @Override
+            public void onRevealViewVisible(View revealView, View contentView,
+                    View allAppsButtonView) {
+                revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark));
+            }
+            @Override
+            public float getMaterialRevealViewFinalAlpha(View revealView) {
+                return 0.3f;
+            }
+            @Override
+            public float getMaterialRevealViewFinalYDrift(View revealView) {
+                return revealView.getMeasuredHeight() / 2;
+            }
+        };
+        startAnimationToOverlay(Workspace.State.OVERVIEW_HIDDEN, toView,
+                toView.getContentView(), toView.getRevealView(), animated, true /* hideSearchBar */,
+                cb);
+    }
+
+    /**
+     * Starts and animation to the workspace from the current overlay view.
+     */
+    public void startAnimationToWorkspace(final Launcher.State fromState,
+              final Workspace.State toWorkspaceState, final boolean animated,
+              final Runnable onCompleteRunnable) {
+        if (toWorkspaceState != Workspace.State.NORMAL &&
+                toWorkspaceState != Workspace.State.SPRING_LOADED &&
+                toWorkspaceState != Workspace.State.OVERVIEW) {
+            Log.e(TAG, "Unexpected call to startAnimationToWorkspace");
+        }
+
+        if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) {
+            startAnimationToWorkspaceFromAllApps(fromState, toWorkspaceState, animated,
+                    onCompleteRunnable);
+        } else {
+            startAnimationToWorkspaceFromWidgets(fromState, toWorkspaceState, animated,
+                    onCompleteRunnable);
+        }
+    }
+
+    /**
+     * Creates and starts a new animation to a particular overlay view.
+     */
+    private void startAnimationToOverlay(final Workspace.State toWorkspaceState, final View toView,
+             final View contentView, final View revealView, final boolean animated,
+             final boolean hideSearchBar, final PrivateTransitionCallbacks pCb) {
+        final Resources res = mLauncher.getResources();
+        final boolean material = Utilities.isLmpOrAbove();
+        final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime);
+        final int itemsAlphaStagger =
+                res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger);
+
+        final View allAppsButtonView = mLauncher.getAllAppsButton();
+        final View fromView = mLauncher.getWorkspace();
+
+        final HashMap<View, Integer> layerViews = new HashMap<>();
+
+        // If for some reason our views aren't initialized, don't animate
+        boolean initialized = allAppsButtonView != null;
+
+        // Cancel the current animation
+        cancelAnimation();
+
+        // Create the workspace animation.
+        // NOTE: this call apparently also sets the state for the workspace if !animated
+        Animator workspaceAnim = mLauncher.getWorkspace().getChangeStateAnimation(
+                toWorkspaceState, animated, layerViews);
+
+        if (animated && initialized) {
+            mStateAnimation = LauncherAnimUtils.createAnimatorSet();
+
+            // Setup the reveal view animation
+            int width = revealView.getMeasuredWidth();
+            int height = revealView.getMeasuredHeight();
+            float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4);
+            revealView.setVisibility(View.VISIBLE);
+            revealView.setAlpha(0f);
+            revealView.setTranslationY(0f);
+            revealView.setTranslationX(0f);
+            pCb.onRevealViewVisible(revealView, contentView, allAppsButtonView);
+
+            // Calculate the final animation values
+            final float revealViewToAlpha;
+            final float revealViewToXDrift;
+            final float revealViewToYDrift;
+            if (material) {
+                revealViewToAlpha = pCb.getMaterialRevealViewFinalAlpha(revealView);
+                revealViewToYDrift = pCb.getMaterialRevealViewFinalYDrift(revealView);
+                revealViewToXDrift = pCb.getMaterialRevealViewFinalXDrift(revealView);
+            } else {
+                revealViewToAlpha = 0f;
+                revealViewToYDrift = 2 * height / 3;
+                revealViewToXDrift = 0;
+            }
+
+            // Create the animators
+            PropertyValuesHolder panelAlpha =
+                    PropertyValuesHolder.ofFloat("alpha", revealViewToAlpha, 1f);
+            PropertyValuesHolder panelDriftY =
+                    PropertyValuesHolder.ofFloat("translationY", revealViewToYDrift, 0);
+            PropertyValuesHolder panelDriftX =
+                    PropertyValuesHolder.ofFloat("translationX", revealViewToXDrift, 0);
+            ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView,
+                    panelAlpha, panelDriftY, panelDriftX);
+            panelAlphaAndDrift.setDuration(revealDuration);
+            panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
+
+            // Play the animation
+            layerViews.put(revealView, BUILD_AND_SET_LAYER);
+            mStateAnimation.play(panelAlphaAndDrift);
+
+            // Setup the animation for the content view
+            contentView.setVisibility(View.VISIBLE);
+            contentView.setAlpha(0f);
+            contentView.setTranslationY(revealViewToYDrift);
+            layerViews.put(contentView, BUILD_AND_SET_LAYER);
+
+            // Create the individual animators
+            ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY",
+                    revealViewToYDrift, 0);
+            pageDrift.setDuration(revealDuration);
+            pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
+            pageDrift.setStartDelay(itemsAlphaStagger);
+            mStateAnimation.play(pageDrift);
+
+            ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f);
+            itemsAlpha.setDuration(revealDuration);
+            itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
+            itemsAlpha.setStartDelay(itemsAlphaStagger);
+            mStateAnimation.play(itemsAlpha);
+
+            if (material) {
+                // Animate the all apps button
+                float startRadius = pCb.getMaterialRevealViewStartFinalRadius();
+                AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener(
+                        revealView, allAppsButtonView);
+                Animator reveal = ViewAnimationUtils.createCircularReveal(revealView, width / 2,
+                        height / 2, startRadius, revealRadius);
+                reveal.setDuration(revealDuration);
+                reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
+                if (listener != null) {
+                    reveal.addListener(listener);
+                }
+                mStateAnimation.play(reveal);
+            }
+
+            mStateAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    dispatchOnLauncherTransitionEnd(fromView, animated, false);
+                    dispatchOnLauncherTransitionEnd(toView, animated, false);
+
+                    // Hide the reveal view
+                    revealView.setVisibility(View.INVISIBLE);
+                    pCb.onAnimationComplete(revealView, contentView, allAppsButtonView);
+
+                    // Disable all necessary layers
+                    for (View v : layerViews.keySet()) {
+                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
+                            v.setLayerType(View.LAYER_TYPE_NONE, null);
+                        }
+                    }
+
+                    if (hideSearchBar) {
+                        mCb.onStateTransitionHideSearchBar();
+                    }
+
+                    // This can hold unnecessary references to views.
+                    mStateAnimation = null;
+                }
+
+            });
+
+            // Play the workspace animation
+            if (workspaceAnim != null) {
+                mStateAnimation.play(workspaceAnim);
+            }
+
+            // Dispatch the prepare transition signal
+            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
+            dispatchOnLauncherTransitionPrepare(toView, animated, false);
+
+
+            final AnimatorSet stateAnimation = mStateAnimation;
+            final Runnable startAnimRunnable = new Runnable() {
+                public void run() {
+                    // Check that mStateAnimation hasn't changed while
+                    // we waited for a layout/draw pass
+                    if (mStateAnimation != stateAnimation)
+                        return;
+                    dispatchOnLauncherTransitionStart(fromView, animated, false);
+                    dispatchOnLauncherTransitionStart(toView, animated, false);
+
+                    // Enable all necessary layers
+                    for (View v : layerViews.keySet()) {
+                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
+                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                        }
+                        if (Utilities.isViewAttachedToWindow(v)) {
+                            v.buildLayer();
+                        }
+                    }
+
+                    // Focus the new view
+                    toView.requestFocus();
+
+                    mStateAnimation.start();
+                }
+            };
+            toView.bringToFront();
+            toView.setVisibility(View.VISIBLE);
+            toView.post(startAnimRunnable);
+        } else {
+            toView.setTranslationX(0.0f);
+            toView.setTranslationY(0.0f);
+            toView.setScaleX(1.0f);
+            toView.setScaleY(1.0f);
+            toView.setVisibility(View.VISIBLE);
+            toView.bringToFront();
+
+            // Show the content view
+            contentView.setVisibility(View.VISIBLE);
+
+            if (hideSearchBar) {
+                mCb.onStateTransitionHideSearchBar();
+            }
+
+            dispatchOnLauncherTransitionPrepare(fromView, animated, false);
+            dispatchOnLauncherTransitionStart(fromView, animated, false);
+            dispatchOnLauncherTransitionEnd(fromView, animated, false);
+            dispatchOnLauncherTransitionPrepare(toView, animated, false);
+            dispatchOnLauncherTransitionStart(toView, animated, false);
+            dispatchOnLauncherTransitionEnd(toView, animated, false);
+        }
+    }
+
+    /**
+     * Starts and animation to the workspace from the apps view.
+     */
+    private void startAnimationToWorkspaceFromAllApps(final Launcher.State fromState,
+              final Workspace.State toWorkspaceState, final boolean animated,
+              final Runnable onCompleteRunnable) {
+        AppsContainerView appsView = mLauncher.getAppsView();
+        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
+            int[] mAllAppsToPanelDelta;
+
+            @Override
+            public void onRevealViewVisible(View revealView, View contentView,
+                                            View allAppsButtonView) {
+                // Get the y delta between the center of the page and the center of the all apps
+                // button
+                mAllAppsToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView,
+                        allAppsButtonView, null);
+            }
+            @Override
+            public float getMaterialRevealViewFinalXDrift(View revealView) {
+                return mAllAppsToPanelDelta[0];
+            }
+            @Override
+            public float getMaterialRevealViewFinalYDrift(View revealView) {
+                return mAllAppsToPanelDelta[1];
+            }
+            @Override
+            float getMaterialRevealViewFinalAlpha(View revealView) {
+                // No alpha anim from all apps
+                return 1f;
+            }
+            @Override
+            float getMaterialRevealViewStartFinalRadius() {
+                int allAppsButtonSize = LauncherAppState.getInstance().
+                        getDynamicGrid().getDeviceProfile().allAppsButtonVisualSize;
+                return allAppsButtonSize / 2;
+            }
+            @Override
+            public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
+                    final View revealView, final View allAppsButtonView) {
+                return new AnimatorListenerAdapter() {
+                    public void onAnimationStart(Animator animation) {
+                        // We set the alpha instead of visibility to ensure that the focus does not
+                        // get taken from the all apps view
+                        allAppsButtonView.setVisibility(View.VISIBLE);
+                        allAppsButtonView.setAlpha(0f);
+                    }
+                    public void onAnimationEnd(Animator animation) {
+                        // Hide the reveal view
+                        revealView.setVisibility(View.INVISIBLE);
+
+                        // Show the all apps button, and focus it
+                        allAppsButtonView.setAlpha(1f);
+                    }
+                };
+            }
+        };
+        startAnimationToWorkspaceFromOverlay(toWorkspaceState, appsView, appsView.getContentView(),
+                appsView.getRevealView(), animated, onCompleteRunnable, cb);
+    }
+
+    /**
+     * Starts and animation to the workspace from the widgets view.
+     */
+    private void startAnimationToWorkspaceFromWidgets(final Launcher.State fromState,
+              final Workspace.State toWorkspaceState, final boolean animated,
+              final Runnable onCompleteRunnable) {
+        final WidgetsContainerView widgetsView = mLauncher.getWidgetsView();
+        final Resources res = mLauncher.getResources();
+        PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks() {
+            @Override
+            public void onRevealViewVisible(View revealView, View contentView,
+                                            View allAppsButtonView) {
+                revealView.setBackground(res.getDrawable(R.drawable.quantum_panel_dark));
+            }
+            @Override
+            public float getMaterialRevealViewFinalYDrift(View revealView) {
+                return revealView.getMeasuredHeight() / 2;
+            }
+            @Override
+            float getMaterialRevealViewFinalAlpha(View revealView) {
+                return 0.4f;
+            }
+            @Override
+            public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
+                    final View revealView, final View allAppsButtonView) {
+                return new AnimatorListenerAdapter() {
+                    public void onAnimationEnd(Animator animation) {
+                        // Hide the reveal view
+                        revealView.setVisibility(View.INVISIBLE);
+                    }
+                };
+            }
+        };
+        startAnimationToWorkspaceFromOverlay(toWorkspaceState, widgetsView,
+                widgetsView.getContentView(), widgetsView.getRevealView(), animated,
+                onCompleteRunnable, cb);
+    }
+
+    /**
+     * Creates and starts a new animation to the workspace.
+     */
+    private void startAnimationToWorkspaceFromOverlay(final Workspace.State toWorkspaceState,
+              final View fromView, final View contentView, final View revealView,
+              final boolean animated, final Runnable onCompleteRunnable,
+              final PrivateTransitionCallbacks pCb) {
+        final Resources res = mLauncher.getResources();
+        final boolean material = Utilities.isLmpOrAbove();
+        final int revealDuration = res.getInteger(R.integer.config_appsCustomizeRevealTime);
+        final int itemsAlphaStagger =
+                res.getInteger(R.integer.config_appsCustomizeItemsAlphaStagger);
+
+        final View allAppsButtonView = mLauncher.getAllAppsButton();
+        final View toView = mLauncher.getWorkspace();
+
+        final HashMap<View, Integer> layerViews = new HashMap<>();
+
+        // If for some reason our views aren't initialized, don't animate
+        boolean initialized = allAppsButtonView != null;
+
+        // Cancel the current animation
+        cancelAnimation();
+
+        // Create the workspace animation.
+        // NOTE: this call apparently also sets the state for the workspace if !animated
+        Animator workspaceAnim = mLauncher.getWorkspace().getChangeStateAnimation(
+                toWorkspaceState, animated, layerViews);
+
+        if (animated && initialized) {
+            mStateAnimation = LauncherAnimUtils.createAnimatorSet();
+
+            // Play the workspace animation
+            if (workspaceAnim != null) {
+                mStateAnimation.play(workspaceAnim);
+            }
+
+            // hideAppsCustomizeHelper is called in some cases when it is already hidden
+            // don't perform all these no-op animations. In particularly, this was causing
+            // the all-apps button to pop in and out.
+            if (fromView.getVisibility() == View.VISIBLE) {
+                int width = revealView.getMeasuredWidth();
+                int height = revealView.getMeasuredHeight();
+                float revealRadius = (float) Math.sqrt((width * width) / 4 + (height * height) / 4);
+                revealView.setVisibility(View.VISIBLE);
+                revealView.setAlpha(1f);
+                revealView.setTranslationY(0);
+                layerViews.put(revealView, BUILD_AND_SET_LAYER);
+                pCb.onRevealViewVisible(revealView, contentView, allAppsButtonView);
+
+                // Calculate the final animation values
+                final float revealViewToXDrift;
+                final float revealViewToYDrift;
+                if (material) {
+                    revealViewToYDrift = pCb.getMaterialRevealViewFinalYDrift(revealView);
+                    revealViewToXDrift = pCb.getMaterialRevealViewFinalXDrift(revealView);
+                } else {
+                    revealViewToYDrift = 2 * height / 3;
+                    revealViewToXDrift = 0;
+                }
+
+                // The vertical motion of the apps panel should be delayed by one frame
+                // from the conceal animation in order to give the right feel. We correspondingly
+                // shorten the duration so that the slide and conceal end at the same time.
+                TimeInterpolator decelerateInterpolator = material ?
+                        new LogDecelerateInterpolator(100, 0) :
+                        new DecelerateInterpolator(1f);
+                ObjectAnimator panelDriftY = LauncherAnimUtils.ofFloat(revealView, "translationY",
+                        0, revealViewToYDrift);
+                panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY);
+                panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
+                panelDriftY.setInterpolator(decelerateInterpolator);
+                mStateAnimation.play(panelDriftY);
+
+                ObjectAnimator panelDriftX = LauncherAnimUtils.ofFloat(revealView, "translationX",
+                        0, revealViewToXDrift);
+                panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY);
+                panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
+                panelDriftX.setInterpolator(decelerateInterpolator);
+                mStateAnimation.play(panelDriftX);
+
+                // Setup animation for the reveal panel alpha
+                final float revealViewToAlpha = !material ? 0f :
+                        pCb.getMaterialRevealViewFinalAlpha(revealView);
+                if (revealViewToAlpha != 1f) {
+                    ObjectAnimator panelAlpha = LauncherAnimUtils.ofFloat(revealView, "alpha",
+                            1f, revealViewToAlpha);
+                    panelAlpha.setDuration(material ? revealDuration : 150);
+                    panelAlpha.setStartDelay(material ? 0 : itemsAlphaStagger + SINGLE_FRAME_DELAY);
+                    panelAlpha.setInterpolator(decelerateInterpolator);
+                    mStateAnimation.play(panelAlpha);
+                }
+
+                // Setup the animation for the content view
+                layerViews.put(contentView, BUILD_AND_SET_LAYER);
+
+                // Create the individual animators
+                ObjectAnimator pageDrift = LauncherAnimUtils.ofFloat(contentView, "translationY",
+                        0, revealViewToYDrift);
+                contentView.setTranslationY(0);
+                pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY);
+                pageDrift.setInterpolator(decelerateInterpolator);
+                pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
+                mStateAnimation.play(pageDrift);
+
+                contentView.setAlpha(1f);
+                ObjectAnimator itemsAlpha = LauncherAnimUtils.ofFloat(contentView, "alpha", 1f, 0f);
+                itemsAlpha.setDuration(100);
+                itemsAlpha.setInterpolator(decelerateInterpolator);
+                mStateAnimation.play(itemsAlpha);
+
+                if (material) {
+                    // Animate the all apps button
+                    float finalRadius = pCb.getMaterialRevealViewStartFinalRadius();
+                    AnimatorListenerAdapter listener =
+                            pCb.getMaterialRevealViewAnimatorListener(revealView, allAppsButtonView);
+                    Animator reveal =
+                            LauncherAnimUtils.createCircularReveal(revealView, width / 2,
+                                    height / 2, revealRadius, finalRadius);
+                    reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
+                    reveal.setDuration(revealDuration);
+                    reveal.setStartDelay(itemsAlphaStagger);
+                    if (listener != null) {
+                        reveal.addListener(listener);
+                    }
+                    mStateAnimation.play(reveal);
+                }
+
+                dispatchOnLauncherTransitionPrepare(fromView, animated, true);
+                dispatchOnLauncherTransitionPrepare(toView, animated, true);
+            }
+
+            mStateAnimation.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    fromView.setVisibility(View.GONE);
+                    dispatchOnLauncherTransitionEnd(fromView, animated, true);
+                    dispatchOnLauncherTransitionEnd(toView, animated, true);
+
+                    // Run any queued runnables
+                    if (onCompleteRunnable != null) {
+                        onCompleteRunnable.run();
+                    }
+
+                    // Animation complete callback
+                    pCb.onAnimationComplete(revealView, contentView, allAppsButtonView);
+
+                    // Disable all necessary layers
+                    for (View v : layerViews.keySet()) {
+                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
+                            v.setLayerType(View.LAYER_TYPE_NONE, null);
+                        }
+                    }
+
+                    // Reset page transforms
+                    if (contentView != null) {
+                        contentView.setTranslationX(0);
+                        contentView.setTranslationY(0);
+                        contentView.setAlpha(1);
+                    }
+
+                    // This can hold unnecessary references to views.
+                    mStateAnimation = null;
+                }
+            });
+
+            final AnimatorSet stateAnimation = mStateAnimation;
+            final Runnable startAnimRunnable = new Runnable() {
+                public void run() {
+                    // Check that mStateAnimation hasn't changed while
+                    // we waited for a layout/draw pass
+                    if (mStateAnimation != stateAnimation)
+                        return;
+                    dispatchOnLauncherTransitionStart(fromView, animated, false);
+                    dispatchOnLauncherTransitionStart(toView, animated, false);
+
+                    // Enable all necessary layers
+                    for (View v : layerViews.keySet()) {
+                        if (layerViews.get(v) == BUILD_AND_SET_LAYER) {
+                            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                        }
+                        if (Utilities.isLmpOrAbove()) {
+                            v.buildLayer();
+                        }
+                    }
+                    mStateAnimation.start();
+                }
+            };
+            fromView.post(startAnimRunnable);
+        } else {
+            fromView.setVisibility(View.GONE);
+            dispatchOnLauncherTransitionPrepare(fromView, animated, true);
+            dispatchOnLauncherTransitionStart(fromView, animated, true);
+            dispatchOnLauncherTransitionEnd(fromView, animated, true);
+            dispatchOnLauncherTransitionPrepare(toView, animated, true);
+            dispatchOnLauncherTransitionStart(toView, animated, true);
+            dispatchOnLauncherTransitionEnd(toView, animated, true);
+
+            // Run any queued runnables
+            if (onCompleteRunnable != null) {
+                onCompleteRunnable.run();
+            }
+        }
+    }
+
+
+    /**
+     * Dispatches the prepare-transition event to suitable views.
+     */
+    void dispatchOnLauncherTransitionPrepare(View v, boolean animated, boolean toWorkspace) {
+        if (v instanceof LauncherTransitionable) {
+            ((LauncherTransitionable) v).onLauncherTransitionPrepare(mLauncher, animated,
+                    toWorkspace);
+        }
+    }
+
+    /**
+     * Dispatches the start-transition event to suitable views.
+     */
+    void dispatchOnLauncherTransitionStart(View v, boolean animated, boolean toWorkspace) {
+        if (v instanceof LauncherTransitionable) {
+            ((LauncherTransitionable) v).onLauncherTransitionStart(mLauncher, animated,
+                    toWorkspace);
+        }
+
+        // Update the workspace transition step as well
+        dispatchOnLauncherTransitionStep(v, 0f);
+    }
+
+    /**
+     * Dispatches the step-transition event to suitable views.
+     */
+    void dispatchOnLauncherTransitionStep(View v, float t) {
+        if (v instanceof LauncherTransitionable) {
+            ((LauncherTransitionable) v).onLauncherTransitionStep(mLauncher, t);
+        }
+    }
+
+    /**
+     * Dispatches the end-transition event to suitable views.
+     */
+    void dispatchOnLauncherTransitionEnd(View v, boolean animated, boolean toWorkspace) {
+        if (v instanceof LauncherTransitionable) {
+            ((LauncherTransitionable) v).onLauncherTransitionEnd(mLauncher, animated,
+                    toWorkspace);
+        }
+
+        // Update the workspace transition step as well
+        dispatchOnLauncherTransitionStep(v, 1f);
+    }
+
+    /**
+     * Cancels the current animation.
+     */
+    private void cancelAnimation() {
+        if (mStateAnimation != null) {
+            mStateAnimation.setDuration(0);
+            mStateAnimation.cancel();
+            mStateAnimation = null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/PackageChangedReceiver.java b/src/com/android/launcher3/PackageChangedReceiver.java
deleted file mode 100644
index e59f6d8..0000000
--- a/src/com/android/launcher3/PackageChangedReceiver.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.android.launcher3;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class PackageChangedReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(final Context context, Intent intent) {
-        final String packageName = intent.getData().getSchemeSpecificPart();
-
-        if (packageName == null || packageName.length() == 0) {
-            // they sent us a bad intent
-            return;
-        }
-        // in rare cases the receiver races with the application to set up LauncherAppState
-        LauncherAppState.setApplicationContext(context.getApplicationContext());
-        LauncherAppState app = LauncherAppState.getInstance();
-        WidgetPreviewLoader.removePackageFromDb(app.getWidgetPreviewCacheDb(), packageName);
-    }
-}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 7d65f46..88295c0 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -19,6 +19,7 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
@@ -51,6 +52,8 @@
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
 
+import com.android.launcher3.util.Thunk;
+
 import java.util.ArrayList;
 
 interface Page {
@@ -124,7 +127,7 @@
     protected LauncherScroller mScroller;
     private Interpolator mDefaultInterpolator;
     private VelocityTracker mVelocityTracker;
-    private int mPageSpacing = 0;
+    @Thunk int mPageSpacing = 0;
 
     private float mParentDownMotionX;
     private float mParentDownMotionY;
@@ -207,8 +210,8 @@
     private boolean mWasInOverscroll = false;
 
     // Page Indicator
-    private int mPageIndicatorViewId;
-    private PageIndicator mPageIndicator;
+    @Thunk int mPageIndicatorViewId;
+    @Thunk PageIndicator mPageIndicator;
     private boolean mAllowPagedViewAnimations = true;
 
     // The viewport whether the pages are to be contained (the actual view may be larger than the
@@ -227,7 +230,7 @@
     protected View mDragView;
     protected AnimatorSet mZoomInOutAnim;
     private Runnable mSidePageHoverRunnable;
-    private int mSidePageHoverIndex = -1;
+    @Thunk int mSidePageHoverIndex = -1;
     // This variable's scope is only for the duration of startReordering() and endReordering()
     private boolean mReorderingStarted = false;
     // This variable's scope is for the duration of startReordering() and after the zoomIn()
@@ -246,14 +249,14 @@
     private Rect mAltTmpRect = new Rect();
 
     // Fling to delete
-    private int FLING_TO_DELETE_FADE_OUT_DURATION = 350;
+    @Thunk int FLING_TO_DELETE_FADE_OUT_DURATION = 350;
     private float FLING_TO_DELETE_FRICTION = 0.035f;
     // The degrees specifies how much deviation from the up vector to still consider a fling "up"
     private float FLING_TO_DELETE_MAX_FLING_DEGREES = 65f;
     protected int mFlingToDeleteThresholdVelocity = -1400;
     // Drag to delete
-    private boolean mDeferringForDelete = false;
-    private int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250;
+    @Thunk boolean mDeferringForDelete = false;
+    @Thunk int DELETE_SLIDE_IN_SIDE_PAGE_DURATION = 250;
     private int DRAG_TO_DELETE_FADE_OUT_DURATION = 350;
 
     // Drop to delete
@@ -473,13 +476,14 @@
 
     /**
      * Returns the index of the currently displayed page.
-     *
-     * @return The index of the currently displayed page.
      */
     int getCurrentPage() {
         return mCurrentPage;
     }
 
+    /**
+     * Returns the index of page to be shown immediately afterwards.
+     */
     int getNextPage() {
         return (mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage;
     }
@@ -959,8 +963,8 @@
         LayoutParams nextLp;
 
         int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft());
-        if (mPageScrolls == null || getChildCount() != mChildCountOnLastLayout) {
-            mPageScrolls = new int[getChildCount()];
+        if (mPageScrolls == null || childCount != mChildCountOnLastLayout) {
+            mPageScrolls = new int[childCount];
         }
 
         for (int i = startIndex; i != endIndex; i += delta) {
@@ -1007,19 +1011,36 @@
             }
         }
 
-        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
+        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < childCount) {
             updateCurrentPageScroll();
             mFirstLayout = false;
         }
 
-        if (childCount > 0) {
-            final int index = isLayoutRtl() ? 0 : childCount - 1;
-            mMaxScrollX = getScrollForPage(index);
+        final LayoutTransition transition = getLayoutTransition();
+        // If the transition is running defer updating max scroll, as some empty pages could
+        // still be present, and a max scroll change could cause sudden jumps in scroll.
+        if (transition != null && transition.isRunning()) {
+            transition.addTransitionListener(new LayoutTransition.TransitionListener() {
+
+                @Override
+                public void startTransition(LayoutTransition transition, ViewGroup container,
+                        View view, int transitionType) { }
+
+                @Override
+                public void endTransition(LayoutTransition transition, ViewGroup container,
+                        View view, int transitionType) {
+                    // Wait until all transitions are complete.
+                    if (!transition.isRunning()) {
+                        transition.removeTransitionListener(this);
+                        updateMaxScrollX();
+                    }
+                }
+            });
         } else {
-            mMaxScrollX = 0;
+            updateMaxScrollX();
         }
 
-        if (mScroller.isFinished() && mChildCountOnLastLayout != getChildCount() &&
+        if (mScroller.isFinished() && mChildCountOnLastLayout != childCount &&
                 !mDeferringForDelete) {
             if (mRestorePage != INVALID_RESTORE_PAGE) {
                 setCurrentPage(mRestorePage);
@@ -1028,13 +1049,23 @@
                 setCurrentPage(getNextPage());
             }
         }
-        mChildCountOnLastLayout = getChildCount();
+        mChildCountOnLastLayout = childCount;
 
         if (isReordering(true)) {
             updateDragViewTranslationDuringDrag();
         }
     }
 
+    private void updateMaxScrollX() {
+        int childCount = getChildCount();
+        if (childCount > 0) {
+            final int index = isLayoutRtl() ? 0 : childCount - 1;
+            mMaxScrollX = getScrollForPage(index);
+        } else {
+            mMaxScrollX = 0;
+        }
+    }
+
     public void setPageSpacing(int pageSpacing) {
         mPageSpacing = pageSpacing;
         requestLayout();
@@ -1700,7 +1731,7 @@
         setEnableOverscroll(!freeScroll);
     }
 
-    private void setEnableOverscroll(boolean enable) {
+    protected void setEnableOverscroll(boolean enable) {
         mAllowOverScroll = enable;
     }
 
@@ -2356,7 +2387,7 @@
             super(superState);
         }
 
-        private SavedState(Parcel in) {
+        @Thunk SavedState(Parcel in) {
             super(in);
             currentPage = in.readInt();
         }
@@ -2514,7 +2545,7 @@
         invalidate();
     }
 
-    private void onPostReorderingAnimationCompleted() {
+    @Thunk void onPostReorderingAnimationCompleted() {
         // Trigger the callback when reordering has settled
         --mPostReorderingPreZoomInRemainingAnimationCount;
         if (mPostReorderingPreZoomInRunnable != null &&
diff --git a/src/com/android/launcher3/PagedViewCellLayout.java b/src/com/android/launcher3/PagedViewCellLayout.java
deleted file mode 100644
index 2d9e10b..0000000
--- a/src/com/android/launcher3/PagedViewCellLayout.java
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
- * Copyright (C) 2010 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.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewDebug;
-import android.view.ViewGroup;
-
-/**
- * An abstraction of the original CellLayout which supports laying out items
- * which span multiple cells into a grid-like layout.  Also supports dimming
- * to give a preview of its contents.
- */
-public class PagedViewCellLayout extends ViewGroup implements Page {
-    static final String TAG = "PagedViewCellLayout";
-
-    private int mCellCountX;
-    private int mCellCountY;
-    private int mOriginalCellWidth;
-    private int mOriginalCellHeight;
-    private int mCellWidth;
-    private int mCellHeight;
-    private int mOriginalWidthGap;
-    private int mOriginalHeightGap;
-    private int mWidthGap;
-    private int mHeightGap;
-    protected PagedViewCellLayoutChildren mChildren;
-
-    public PagedViewCellLayout(Context context) {
-        this(context, null);
-    }
-
-    public PagedViewCellLayout(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public PagedViewCellLayout(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        setAlwaysDrawnWithCacheEnabled(false);
-
-        // setup default cell parameters
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-        mOriginalCellWidth = mCellWidth = grid.cellWidthPx;
-        mOriginalCellHeight = mCellHeight = grid.cellHeightPx;
-        mCellCountX = (int) grid.numColumns;
-        mCellCountY = (int) grid.numRows;
-        mOriginalWidthGap = mOriginalHeightGap = mWidthGap = mHeightGap = -1;
-
-        mChildren = new PagedViewCellLayoutChildren(context);
-        mChildren.setCellDimensions(mCellWidth, mCellHeight);
-        mChildren.setGap(mWidthGap, mHeightGap);
-
-        addView(mChildren);
-    }
-
-    public int getCellWidth() {
-        return mCellWidth;
-    }
-
-    public int getCellHeight() {
-        return mCellHeight;
-    }
-
-    @Override
-    public void cancelLongPress() {
-        super.cancelLongPress();
-
-        // Cancel long press for all children
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            final View child = getChildAt(i);
-            child.cancelLongPress();
-        }
-    }
-
-    public boolean addViewToCellLayout(View child, int index, int childId,
-            PagedViewCellLayout.LayoutParams params) {
-        final PagedViewCellLayout.LayoutParams lp = params;
-
-        // Generate an id for each view, this assumes we have at most 256x256 cells
-        // per workspace screen
-        if (lp.cellX >= 0 && lp.cellX <= (mCellCountX - 1) &&
-                lp.cellY >= 0 && (lp.cellY <= mCellCountY - 1)) {
-            // If the horizontal or vertical span is set to -1, it is taken to
-            // mean that it spans the extent of the CellLayout
-            if (lp.cellHSpan < 0) lp.cellHSpan = mCellCountX;
-            if (lp.cellVSpan < 0) lp.cellVSpan = mCellCountY;
-
-            child.setId(childId);
-            mChildren.addView(child, index, lp);
-
-            return true;
-        }
-        return false;
-    }
-
-    @Override
-    public void removeAllViewsOnPage() {
-        mChildren.removeAllViews();
-        setLayerType(LAYER_TYPE_NONE, null);
-    }
-
-    @Override
-    public void removeViewOnPageAt(int index) {
-        mChildren.removeViewAt(index);
-    }
-
-    /**
-     * Clears all the key listeners for the individual icons.
-     */
-    public void resetChildrenOnKeyListeners() {
-        int childCount = mChildren.getChildCount();
-        for (int j = 0; j < childCount; ++j) {
-            mChildren.getChildAt(j).setOnKeyListener(null);
-        }
-    }
-
-    @Override
-    public int getPageChildCount() {
-        return mChildren.getChildCount();
-    }
-
-    public PagedViewCellLayoutChildren getChildrenLayout() {
-        return mChildren;
-    }
-
-    @Override
-    public View getChildOnPageAt(int i) {
-        return mChildren.getChildAt(i);
-    }
-
-    @Override
-    public int indexOfChildOnPage(View v) {
-        return mChildren.indexOfChild(v);
-    }
-
-    public int getCellCountX() {
-        return mCellCountX;
-    }
-
-    public int getCellCountY() {
-        return mCellCountY;
-    }
-
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
-        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
-
-        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
-        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
-
-        if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
-            throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
-        }
-
-        int numWidthGaps = mCellCountX - 1;
-        int numHeightGaps = mCellCountY - 1;
-
-        if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) {
-            int hSpace = widthSpecSize - getPaddingLeft() - getPaddingRight();
-            int vSpace = heightSpecSize - getPaddingTop() - getPaddingBottom();
-            int hFreeSpace = hSpace - (mCellCountX * mOriginalCellWidth);
-            int vFreeSpace = vSpace - (mCellCountY * mOriginalCellHeight);
-            mWidthGap = numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0;
-            mHeightGap = numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0;
-
-            mChildren.setGap(mWidthGap, mHeightGap);
-        } else {
-            mWidthGap = mOriginalWidthGap;
-            mHeightGap = mOriginalHeightGap;
-        }
-
-        // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY
-        int newWidth = widthSpecSize;
-        int newHeight = heightSpecSize;
-        if (widthSpecMode == MeasureSpec.AT_MOST) {
-            newWidth = getPaddingLeft() + getPaddingRight() + (mCellCountX * mCellWidth) +
-                ((mCellCountX - 1) * mWidthGap);
-            newHeight = getPaddingTop() + getPaddingBottom() + (mCellCountY * mCellHeight) +
-                ((mCellCountY - 1) * mHeightGap);
-            setMeasuredDimension(newWidth, newHeight);
-        }
-
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            View child = getChildAt(i);
-            int childWidthMeasureSpec =
-                MeasureSpec.makeMeasureSpec(newWidth - getPaddingLeft() -
-                        getPaddingRight(), MeasureSpec.EXACTLY);
-            int childheightMeasureSpec =
-                MeasureSpec.makeMeasureSpec(newHeight - getPaddingTop() -
-                        getPaddingBottom(), MeasureSpec.EXACTLY);
-            child.measure(childWidthMeasureSpec, childheightMeasureSpec);
-        }
-
-        setMeasuredDimension(newWidth, newHeight);
-    }
-
-    int getContentWidth() {
-        return getWidthBeforeFirstLayout() + getPaddingLeft() + getPaddingRight();
-    }
-
-    int getContentHeight() {
-        if (mCellCountY > 0) {
-            return mCellCountY * mCellHeight + (mCellCountY - 1) * Math.max(0, mHeightGap);
-        }
-        return 0;
-    }
-
-    int getWidthBeforeFirstLayout() {
-        if (mCellCountX > 0) {
-            return mCellCountX * mCellWidth + (mCellCountX - 1) * Math.max(0, mWidthGap);
-        }
-        return 0;
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            View child = getChildAt(i);
-            child.layout(getPaddingLeft(), getPaddingTop(),
-                r - l - getPaddingRight(), b - t - getPaddingBottom());
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        boolean result = super.onTouchEvent(event);
-        int count = getPageChildCount();
-        if (count > 0) {
-            // We only intercept the touch if we are tapping in empty space after the final row
-            View child = getChildOnPageAt(count - 1);
-            int bottom = child.getBottom();
-            int numRows = (int) Math.ceil((float) getPageChildCount() / getCellCountX());
-            if (numRows < getCellCountY()) {
-                // Add a little bit of buffer if there is room for another row
-                bottom += mCellHeight / 2;
-            }
-            result = result || (event.getY() < bottom);
-        }
-        return result;
-    }
-
-    public void enableCenteredContent(boolean enabled) {
-        mChildren.enableCenteredContent(enabled);
-    }
-
-    @Override
-    protected void setChildrenDrawingCacheEnabled(boolean enabled) {
-        mChildren.setChildrenDrawingCacheEnabled(enabled);
-    }
-
-    public void setCellCount(int xCount, int yCount) {
-        mCellCountX = xCount;
-        mCellCountY = yCount;
-        requestLayout();
-    }
-
-    public void setGap(int widthGap, int heightGap) {
-        mOriginalWidthGap = mWidthGap = widthGap;
-        mOriginalHeightGap = mHeightGap = heightGap;
-        mChildren.setGap(widthGap, heightGap);
-    }
-
-    public int[] getCellCountForDimensions(int width, int height) {
-        // Always assume we're working with the smallest span to make sure we
-        // reserve enough space in both orientations
-        int smallerSize = Math.min(mCellWidth, mCellHeight);
-
-        // Always round up to next largest cell
-        int spanX = (width + smallerSize) / smallerSize;
-        int spanY = (height + smallerSize) / smallerSize;
-
-        return new int[] { spanX, spanY };
-    }
-
-    /**
-     * Start dragging the specified child
-     *
-     * @param child The child that is being dragged
-     */
-    void onDragChild(View child) {
-        PagedViewCellLayout.LayoutParams lp = (PagedViewCellLayout.LayoutParams) child.getLayoutParams();
-        lp.isDragging = true;
-    }
-
-    /**
-     * Estimates the number of cells that the specified width would take up.
-     */
-    public int estimateCellHSpan(int width) {
-        // We don't show the next/previous pages any more, so we use the full width, minus the
-        // padding
-        int availWidth = width - (getPaddingLeft() + getPaddingRight());
-
-        // We know that we have to fit N cells with N-1 width gaps, so we just juggle to solve for N
-        int n = Math.max(1, (availWidth + mWidthGap) / (mCellWidth + mWidthGap));
-
-        // We don't do anything fancy to determine if we squeeze another row in.
-        return n;
-    }
-
-    /**
-     * Estimates the number of cells that the specified height would take up.
-     */
-    public int estimateCellVSpan(int height) {
-        // The space for a page is the height - top padding (current page) - bottom padding (current
-        // page)
-        int availHeight = height - (getPaddingTop() + getPaddingBottom());
-
-        // We know that we have to fit N cells with N-1 height gaps, so we juggle to solve for N
-        int n = Math.max(1, (availHeight + mHeightGap) / (mCellHeight + mHeightGap));
-
-        // We don't do anything fancy to determine if we squeeze another row in.
-        return n;
-    }
-
-    /** Returns an estimated center position of the cell at the specified index */
-    public int[] estimateCellPosition(int x, int y) {
-        return new int[] {
-                getPaddingLeft() + (x * mCellWidth) + (x * mWidthGap) + (mCellWidth / 2),
-                getPaddingTop() + (y * mCellHeight) + (y * mHeightGap) + (mCellHeight / 2)
-        };
-    }
-
-    public void calculateCellCount(int width, int height, int maxCellCountX, int maxCellCountY) {
-        mCellCountX = Math.min(maxCellCountX, estimateCellHSpan(width));
-        mCellCountY = Math.min(maxCellCountY, estimateCellVSpan(height));
-        requestLayout();
-    }
-
-    /**
-     * Estimates the width that the number of hSpan cells will take up.
-     */
-    public int estimateCellWidth(int hSpan) {
-        // TODO: we need to take widthGap into effect
-        return hSpan * mCellWidth;
-    }
-
-    /**
-     * Estimates the height that the number of vSpan cells will take up.
-     */
-    public int estimateCellHeight(int vSpan) {
-        // TODO: we need to take heightGap into effect
-        return vSpan * mCellHeight;
-    }
-
-    @Override
-    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
-        return new PagedViewCellLayout.LayoutParams(getContext(), attrs);
-    }
-
-    @Override
-    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
-        return p instanceof PagedViewCellLayout.LayoutParams;
-    }
-
-    @Override
-    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
-        return new PagedViewCellLayout.LayoutParams(p);
-    }
-
-    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
-        /**
-         * Horizontal location of the item in the grid.
-         */
-        @ViewDebug.ExportedProperty
-        public int cellX;
-
-        /**
-         * Vertical location of the item in the grid.
-         */
-        @ViewDebug.ExportedProperty
-        public int cellY;
-
-        /**
-         * Number of cells spanned horizontally by the item.
-         */
-        @ViewDebug.ExportedProperty
-        public int cellHSpan;
-
-        /**
-         * Number of cells spanned vertically by the item.
-         */
-        @ViewDebug.ExportedProperty
-        public int cellVSpan;
-
-        /**
-         * Is this item currently being dragged
-         */
-        public boolean isDragging;
-
-        // a data object that you can bind to this layout params
-        private Object mTag;
-
-        // X coordinate of the view in the layout.
-        @ViewDebug.ExportedProperty
-        int x;
-        // Y coordinate of the view in the layout.
-        @ViewDebug.ExportedProperty
-        int y;
-
-        public LayoutParams() {
-            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
-            cellHSpan = 1;
-            cellVSpan = 1;
-        }
-
-        public LayoutParams(Context c, AttributeSet attrs) {
-            super(c, attrs);
-            cellHSpan = 1;
-            cellVSpan = 1;
-        }
-
-        public LayoutParams(ViewGroup.LayoutParams source) {
-            super(source);
-            cellHSpan = 1;
-            cellVSpan = 1;
-        }
-
-        public LayoutParams(LayoutParams source) {
-            super(source);
-            this.cellX = source.cellX;
-            this.cellY = source.cellY;
-            this.cellHSpan = source.cellHSpan;
-            this.cellVSpan = source.cellVSpan;
-        }
-
-        public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {
-            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
-            this.cellX = cellX;
-            this.cellY = cellY;
-            this.cellHSpan = cellHSpan;
-            this.cellVSpan = cellVSpan;
-        }
-
-        public void setup(Context context,
-                          int cellWidth, int cellHeight, int widthGap, int heightGap,
-                          int hStartPadding, int vStartPadding) {
-
-            final int myCellHSpan = cellHSpan;
-            final int myCellVSpan = cellVSpan;
-            final int myCellX = cellX;
-            final int myCellY = cellY;
-
-            width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) -
-                    leftMargin - rightMargin;
-            height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) -
-                    topMargin - bottomMargin;
-
-            if (LauncherAppState.getInstance().isScreenLarge()) {
-                x = hStartPadding + myCellX * (cellWidth + widthGap) + leftMargin;
-                y = vStartPadding + myCellY * (cellHeight + heightGap) + topMargin;
-            } else {
-                x = myCellX * (cellWidth + widthGap) + leftMargin;
-                y = myCellY * (cellHeight + heightGap) + topMargin;
-            }
-        }
-
-        public Object getTag() {
-            return mTag;
-        }
-
-        public void setTag(Object tag) {
-            mTag = tag;
-        }
-
-        public String toString() {
-            return "(" + this.cellX + ", " + this.cellY + ", " +
-                this.cellHSpan + ", " + this.cellVSpan + ")";
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/PagedViewCellLayoutChildren.java b/src/com/android/launcher3/PagedViewCellLayoutChildren.java
deleted file mode 100644
index 84d2b1d..0000000
--- a/src/com/android/launcher3/PagedViewCellLayoutChildren.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2010 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.Rect;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * An abstraction of the original CellLayout which supports laying out items
- * which span multiple cells into a grid-like layout.  Also supports dimming
- * to give a preview of its contents.
- */
-public class PagedViewCellLayoutChildren extends ViewGroup {
-    static final String TAG = "PagedViewCellLayout";
-
-    private boolean mCenterContent;
-
-    private int mCellWidth;
-    private int mCellHeight;
-    private int mWidthGap;
-    private int mHeightGap;
-
-    public PagedViewCellLayoutChildren(Context context) {
-        super(context);
-    }
-
-    @Override
-    public void cancelLongPress() {
-        super.cancelLongPress();
-
-        // Cancel long press for all children
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            final View child = getChildAt(i);
-            child.cancelLongPress();
-        }
-    }
-
-    public void setGap(int widthGap, int heightGap) {
-        mWidthGap = widthGap;
-        mHeightGap = heightGap;
-        requestLayout();
-    }
-
-    public void setCellDimensions(int width, int height) {
-        mCellWidth = width;
-        mCellHeight = height;
-        requestLayout();
-    }
-
-    @Override
-    public void requestChildFocus(View child, View focused) {
-        super.requestChildFocus(child, focused);
-        if (child != null) {
-            Rect r = new Rect();
-            child.getDrawingRect(r);
-            requestRectangleOnScreen(r);
-        }
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
-        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
-
-        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
-        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
-
-        if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
-            throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions");
-        }
-
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            View child = getChildAt(i);
-            PagedViewCellLayout.LayoutParams lp =
-                (PagedViewCellLayout.LayoutParams) child.getLayoutParams();
-            lp.setup(getContext(),
-                    mCellWidth, mCellHeight, mWidthGap, mHeightGap,
-                    getPaddingLeft(),
-                    getPaddingTop());
-
-            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width,
-                    MeasureSpec.EXACTLY);
-            int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height,
-                    MeasureSpec.EXACTLY);
-
-            child.measure(childWidthMeasureSpec, childheightMeasureSpec);
-        }
-
-        setMeasuredDimension(widthSpecSize, heightSpecSize);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        int count = getChildCount();
-
-        int offsetX = 0;
-        if (mCenterContent && count > 0) {
-            // determine the max width of all the rows and center accordingly
-            int maxRowX = 0;
-            int minRowX = Integer.MAX_VALUE;
-            for (int i = 0; i < count; i++) {
-                View child = getChildAt(i);
-                if (child.getVisibility() != GONE) {
-                    PagedViewCellLayout.LayoutParams lp =
-                        (PagedViewCellLayout.LayoutParams) child.getLayoutParams();
-                    minRowX = Math.min(minRowX, lp.x);
-                    maxRowX = Math.max(maxRowX, lp.x + lp.width);
-                }
-            }
-            int maxRowWidth = maxRowX - minRowX;
-            offsetX = (getMeasuredWidth() - maxRowWidth) / 2;
-        }
-
-        for (int i = 0; i < count; i++) {
-            View child = getChildAt(i);
-            if (child.getVisibility() != GONE) {
-                PagedViewCellLayout.LayoutParams lp =
-                    (PagedViewCellLayout.LayoutParams) child.getLayoutParams();
-
-                int childLeft = offsetX + lp.x;
-                int childTop = lp.y;
-                child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
-            }
-        }
-    }
-
-    public void enableCenteredContent(boolean enabled) {
-        mCenterContent = enabled;
-    }
-
-    @Override
-    protected void setChildrenDrawingCacheEnabled(boolean enabled) {
-        final int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            final View view = getChildAt(i);
-            view.setDrawingCacheEnabled(enabled);
-            // Update the drawing caches
-            if (!view.isHardwareAccelerated()) {
-                view.buildDrawingCache(true);
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/PagedViewGridLayout.java b/src/com/android/launcher3/PagedViewGridLayout.java
deleted file mode 100644
index f69fa56..0000000
--- a/src/com/android/launcher3/PagedViewGridLayout.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2011 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.view.MotionEvent;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.GridLayout;
-
-/**
- * The grid based layout used strictly for the widget/wallpaper tab of the AppsCustomize pane
- */
-public class PagedViewGridLayout extends GridLayout implements Page {
-    static final String TAG = "PagedViewGridLayout";
-
-    private int mCellCountX;
-    private int mCellCountY;
-    private Runnable mOnLayoutListener;
-
-    public PagedViewGridLayout(Context context, int cellCountX, int cellCountY) {
-        super(context, null, 0);
-        mCellCountX = cellCountX;
-        mCellCountY = cellCountY;
-    }
-
-    int getCellCountX() {
-        return mCellCountX;
-    }
-
-    int getCellCountY() {
-        return mCellCountY;
-    }
-
-    /**
-     * Clears all the key listeners for the individual widgets.
-     */
-    public void resetChildrenOnKeyListeners() {
-        int childCount = getChildCount();
-        for (int j = 0; j < childCount; ++j) {
-            getChildAt(j).setOnKeyListener(null);
-        }
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        mOnLayoutListener = null;
-    }
-
-    public void setOnLayoutListener(Runnable r) {
-        mOnLayoutListener = r;
-    }
-
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        if (mOnLayoutListener != null) {
-            mOnLayoutListener.run();
-        }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        boolean result = super.onTouchEvent(event);
-        int count = getPageChildCount();
-        if (count > 0) {
-            // We only intercept the touch if we are tapping in empty space after the final row
-            View child = getChildOnPageAt(count - 1);
-            int bottom = child.getBottom();
-            result = result || (event.getY() < bottom);
-        }
-        return result;
-    }
-
-    @Override
-    public void removeAllViewsOnPage() {
-        removeAllViews();
-        mOnLayoutListener = null;
-        setLayerType(LAYER_TYPE_NONE, null);
-    }
-
-    @Override
-    public void removeViewOnPageAt(int index) {
-        removeViewAt(index);
-    }
-
-    @Override
-    public int getPageChildCount() {
-        return getChildCount();
-    }
-
-    @Override
-    public View getChildOnPageAt(int i) {
-        return getChildAt(i);
-    }
-
-    @Override
-    public int indexOfChildOnPage(View v) {
-        return indexOfChild(v);
-    }
-
-    public static class LayoutParams extends FrameLayout.LayoutParams {
-        public LayoutParams(int width, int height) {
-            super(width, height);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/PagedViewWithDraggableItems.java b/src/com/android/launcher3/PagedViewWithDraggableItems.java
deleted file mode 100644
index 0e59369..0000000
--- a/src/com/android/launcher3/PagedViewWithDraggableItems.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Copyright (C) 2010 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.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-
-
-/* Class that does most of the work of enabling dragging items out of a PagedView by performing a
- * vertical drag. Used by both CustomizePagedView and AllAppsPagedView.
- * Subclasses must do the following:
- *   * call setDragSlopeThreshold after making an instance of the PagedViewWithDraggableItems
- *   * call child.setOnLongClickListener(this) and child.setOnTouchListener(this) on all children
- *       (good place to do it is in syncPageItems)
- *   * override beginDragging(View) (but be careful to call super.beginDragging(View)
- *
- */
-public abstract class PagedViewWithDraggableItems extends PagedView
-    implements View.OnLongClickListener, View.OnTouchListener {
-    private View mLastTouchedItem;
-    private boolean mIsDragging;
-    private boolean mIsDragEnabled;
-    private float mDragSlopeThreshold;
-    private Launcher mLauncher;
-
-    public PagedViewWithDraggableItems(Context context) {
-        this(context, null);
-    }
-
-    public PagedViewWithDraggableItems(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public PagedViewWithDraggableItems(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-        mLauncher = (Launcher) context;
-    }
-
-    protected boolean beginDragging(View v) {
-        boolean wasDragging = mIsDragging;
-        mIsDragging = true;
-        return !wasDragging;
-    }
-
-    protected void cancelDragging() {
-        mIsDragging = false;
-        mLastTouchedItem = null;
-        mIsDragEnabled = false;
-    }
-
-    private void handleTouchEvent(MotionEvent ev) {
-        final int action = ev.getAction();
-        switch (action & MotionEvent.ACTION_MASK) {
-            case MotionEvent.ACTION_DOWN:
-                cancelDragging();
-                mIsDragEnabled = true;
-                break;
-            case MotionEvent.ACTION_MOVE:
-                if (mTouchState != TOUCH_STATE_SCROLLING && !mIsDragging && mIsDragEnabled) {
-                    determineDraggingStart(ev);
-                }
-                break;
-        }
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        handleTouchEvent(ev);
-        return super.onInterceptTouchEvent(ev);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent ev) {
-        handleTouchEvent(ev);
-        return super.onTouchEvent(ev);
-    }
-
-    public void trimMemory() {
-        mLastTouchedItem = null;
-    }
-
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        mLastTouchedItem = v;
-        mIsDragEnabled = true;
-        return false;
-    }
-
-    @Override
-    public boolean onLongClick(View v) {
-        // Return early if this is not initiated from a touch
-        if (!v.isInTouchMode()) return false;
-        // Return early if we are still animating the pages
-        if (mNextPage != INVALID_PAGE) return false;
-        // When we have exited all apps or are in transition, disregard long clicks
-        if (!mLauncher.isAllAppsVisible() ||
-                mLauncher.getWorkspace().isSwitchingState()) return false;
-        // Return if global dragging is not enabled
-        if (!mLauncher.isDraggingEnabled()) return false;
-
-        return beginDragging(v);
-    }
-
-    /*
-     * Determines if we should change the touch state to start scrolling after the
-     * user moves their touch point too far.
-     */
-    protected void determineScrollingStart(MotionEvent ev) {
-        if (!mIsDragging) super.determineScrollingStart(ev);
-    }
-
-    /*
-     * Determines if we should change the touch state to start dragging after the
-     * user moves their touch point far enough.
-     */
-    protected void determineDraggingStart(MotionEvent ev) {
-        /*
-         * Locally do absolute value. mLastMotionX is set to the y value
-         * of the down event.
-         */
-        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
-        final float x = ev.getX(pointerIndex);
-        final float y = ev.getY(pointerIndex);
-        final int xDiff = (int) Math.abs(x - mLastMotionX);
-        final int yDiff = (int) Math.abs(y - mLastMotionY);
-
-        final int touchSlop = mTouchSlop;
-        boolean yMoved = yDiff > touchSlop;
-        boolean isUpwardMotion = (yDiff / (float) xDiff) > mDragSlopeThreshold;
-
-        if (isUpwardMotion && yMoved && mLastTouchedItem != null) {
-            // Drag if the user moved far enough along the Y axis
-            beginDragging(mLastTouchedItem);
-
-            // Cancel any pending long press
-            if (mAllowLongPress) {
-                mAllowLongPress = false;
-                // Try canceling the long press. It could also have been scheduled
-                // by a distant descendant, so use the mAllowLongPress flag to block
-                // everything
-                final View currentPage = getPageAt(mCurrentPage);
-                if (currentPage != null) {
-                    currentPage.cancelLongPress();
-                }
-            }
-        }
-    }
-
-    public void setDragSlopeThreshold(float dragSlopeThreshold) {
-        mDragSlopeThreshold = dragSlopeThreshold;
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        cancelDragging();
-        super.onDetachedFromWindow();
-    }
-}
diff --git a/src/com/android/launcher3/PendingAddItemInfo.java b/src/com/android/launcher3/PendingAddItemInfo.java
index ac54a26..1aaf85b 100644
--- a/src/com/android/launcher3/PendingAddItemInfo.java
+++ b/src/com/android/launcher3/PendingAddItemInfo.java
@@ -16,93 +16,17 @@
 
 package com.android.launcher3;
 
-import android.appwidget.AppWidgetHostView;
 import android.content.ComponentName;
-import android.content.pm.ActivityInfo;
-import android.os.Bundle;
-import android.os.Parcelable;
 
 /**
- * We pass this object with a drag from the customization tray
+ * Meta data that is used for deferred binding.
+ * e.g., this object is used to pass information on dragable targets when they are dropped onto
+ * the workspace from another container.
  */
-class PendingAddItemInfo extends ItemInfo {
+public class PendingAddItemInfo extends ItemInfo {
+
     /**
      * The component that will be created.
      */
-    ComponentName componentName;
-}
-
-class PendingAddShortcutInfo extends PendingAddItemInfo {
-
-    ActivityInfo shortcutActivityInfo;
-
-    public PendingAddShortcutInfo(ActivityInfo activityInfo) {
-        shortcutActivityInfo = activityInfo;
-    }
-
-    @Override
-    public String toString() {
-        return "Shortcut: " + shortcutActivityInfo.packageName;
-    }
-}
-
-class PendingAddWidgetInfo extends PendingAddItemInfo {
-    int minWidth;
-    int minHeight;
-    int minResizeWidth;
-    int minResizeHeight;
-    int previewImage;
-    int icon;
-    LauncherAppWidgetProviderInfo info;
-    AppWidgetHostView boundWidget;
-    Bundle bindOptions = null;
-
-    public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, Parcelable data) {
-        if (i.isCustomWidget) {
-            itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
-        } else {
-            itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
-        }
-        this.info = i;
-        componentName = i.provider;
-        minWidth = i.minWidth;
-        minHeight = i.minHeight;
-        minResizeWidth = i.minResizeWidth;
-        minResizeHeight = i.minResizeHeight;
-        previewImage = i.previewImage;
-        icon = i.icon;
-
-        spanX = i.spanX;
-        spanY = i.spanY;
-        minSpanX = i.minSpanX;
-        minSpanY = i.minSpanY;
-    }
-
-    public boolean isCustomWidget() {
-        return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
-    }
-
-    // Copy constructor
-    public PendingAddWidgetInfo(PendingAddWidgetInfo copy) {
-        minWidth = copy.minWidth;
-        minHeight = copy.minHeight;
-        minResizeWidth = copy.minResizeWidth;
-        minResizeHeight = copy.minResizeHeight;
-        previewImage = copy.previewImage;
-        icon = copy.icon;
-        info = copy.info;
-        boundWidget = copy.boundWidget;
-        componentName = copy.componentName;
-        itemType = copy.itemType;
-        spanX = copy.spanX;
-        spanY = copy.spanY;
-        minSpanX = copy.minSpanX;
-        minSpanY = copy.minSpanY;
-        bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone();
-    }
-
-    @Override
-    public String toString() {
-        return "Widget: " + componentName.toShortString();
-    }
+    public ComponentName componentName;
 }
diff --git a/src/com/android/launcher3/SearchDropTargetBar.java b/src/com/android/launcher3/SearchDropTargetBar.java
index 99c2e08..a8dcd0f 100644
--- a/src/com/android/launcher3/SearchDropTargetBar.java
+++ b/src/com/android/launcher3/SearchDropTargetBar.java
@@ -44,11 +44,14 @@
     private boolean mIsSearchBarHidden;
     private View mQSBSearchBar;
     private View mDropTargetBar;
-    private ButtonDropTarget mInfoDropTarget;
-    private ButtonDropTarget mDeleteDropTarget;
     private int mBarHeight;
     private boolean mDeferOnDragEnd = false;
 
+    // Drop targets
+    private ButtonDropTarget mInfoDropTarget;
+    private ButtonDropTarget mDeleteDropTarget;
+    private ButtonDropTarget mUninstallDropTarget;
+
     private boolean mEnableDropDownDropTargets;
 
     public SearchDropTargetBar(Context context, AttributeSet attrs) {
@@ -61,13 +64,19 @@
 
     public void setup(Launcher launcher, DragController dragController) {
         dragController.addDragListener(this);
+        dragController.setFlingToDeleteDropTarget(mDeleteDropTarget);
+
         dragController.addDragListener(mInfoDropTarget);
         dragController.addDragListener(mDeleteDropTarget);
+        dragController.addDragListener(mUninstallDropTarget);
+
         dragController.addDropTarget(mInfoDropTarget);
         dragController.addDropTarget(mDeleteDropTarget);
-        dragController.setFlingToDeleteDropTarget(mDeleteDropTarget);
+        dragController.addDropTarget(mUninstallDropTarget);
+
         mInfoDropTarget.setLauncher(launcher);
         mDeleteDropTarget.setLauncher(launcher);
+        mUninstallDropTarget.setLauncher(launcher);
     }
 
     public void setQsbSearchBar(View qsb) {
@@ -116,9 +125,11 @@
         mDropTargetBar = findViewById(R.id.drag_target_bar);
         mInfoDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.info_target_text);
         mDeleteDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.delete_target_text);
+        mUninstallDropTarget = (ButtonDropTarget) mDropTargetBar.findViewById(R.id.uninstall_target_text);
 
         mInfoDropTarget.setSearchDropTargetBar(this);
         mDeleteDropTarget.setSearchDropTargetBar(this);
+        mUninstallDropTarget.setSearchDropTargetBar(this);
 
         mEnableDropDownDropTargets =
             getResources().getBoolean(R.bool.config_useDropTargetDownTransition);
@@ -197,6 +208,10 @@
      */
     @Override
     public void onDragStart(DragSource source, Object info, int dragAction) {
+        showDeleteTarget();
+    }
+
+    public void showDeleteTarget() {
         // Animate out the QSB search bar, and animate in the drop target bar
         prepareStartAnimation(mDropTargetBar);
         mDropTargetBarAnim.start();
@@ -206,6 +221,16 @@
         }
     }
 
+    public void hideDeleteTarget() {
+        // Restore the QSB search bar, and animate out the drop target bar
+        prepareStartAnimation(mDropTargetBar);
+        mDropTargetBarAnim.reverse();
+        if (!mIsSearchBarHidden) {
+            prepareStartAnimation(mQSBSearchBar);
+            mQSBSearchBarAnim.reverse();
+        }
+    }
+
     public void deferOnDragEnd() {
         mDeferOnDragEnd = true;
     }
@@ -213,13 +238,7 @@
     @Override
     public void onDragEnd() {
         if (!mDeferOnDragEnd) {
-            // Restore the QSB search bar, and animate out the drop target bar
-            prepareStartAnimation(mDropTargetBar);
-            mDropTargetBarAnim.reverse();
-            if (!mIsSearchBarHidden) {
-                prepareStartAnimation(mQSBSearchBar);
-                mQSBSearchBarAnim.reverse();
-            }
+            hideDeleteTarget();
         } else {
             mDeferOnDragEnd = false;
         }
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index ff06045..15b6176 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -177,7 +177,7 @@
         child.measure(childWidthMeasureSpec, childheightMeasureSpec);
     }
 
-    private boolean invertLayoutHorizontally() {
+    public boolean invertLayoutHorizontally() {
         return mInvertIfRtl && isLayoutRtl();
     }
 
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index 01f7931..5bef845 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -23,7 +23,10 @@
 import android.graphics.Bitmap;
 import android.util.Log;
 
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -46,18 +49,24 @@
      * be present along with {@link #FLAG_RESTORED_ICON}, and is set during default layout
      * parsing.
      */
-    public static final int FLAG_AUTOINTALL_ICON = 2;
+    public static final int FLAG_AUTOINTALL_ICON = 2; //0B10;
 
     /**
      * The icon is being installed. If {@link FLAG_RESTORED_ICON} or {@link FLAG_AUTOINTALL_ICON}
      * is set, then the icon is either being installed or is in a broken state.
      */
-    public static final int FLAG_INSTALL_SESSION_ACTIVE = 4;
+    public static final int FLAG_INSTALL_SESSION_ACTIVE = 4; // 0B100;
 
     /**
      * Indicates that the widget restore has started.
      */
-    public static final int FLAG_RESTORE_STARTED = 8;
+    public static final int FLAG_RESTORE_STARTED = 8; //0B1000;
+
+    /**
+     * Indicates if it represents a common type mentioned in {@link CommonAppTypeParser}.
+     * Upto 15 different types supported.
+     */
+    public static final int FLAG_RESTORED_APP_TYPE = 0B0011110000;
 
     /**
      * The intent used to start the application.
@@ -77,6 +86,11 @@
     boolean usingFallbackIcon;
 
     /**
+     * Indicates whether we're using a low res icon
+     */
+    boolean usingLowResIcon;
+
+    /**
      * If isShortcut=true and customIcon=false, this contains a reference to the
      * shortcut icon as an application's resource.
      */
@@ -184,8 +198,10 @@
     }
 
     public void updateIcon(IconCache iconCache) {
-        mIcon = iconCache.getIcon(promisedIntent != null ? promisedIntent : intent, user);
-        usingFallbackIcon = iconCache.isDefaultIcon(mIcon, user);
+        if (itemType == Favorites.ITEM_TYPE_APPLICATION) {
+            iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user,
+                    shouldUseLowResIcon());
+        }
     }
 
     @Override
@@ -207,9 +223,9 @@
             if (!usingFallbackIcon) {
                 writeBitmap(values, mIcon);
             }
-            values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
-                    LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE);
             if (iconResource != null) {
+                values.put(LauncherSettings.BaseLauncherColumns.ICON_TYPE,
+                        LauncherSettings.BaseLauncherColumns.ICON_TYPE_RESOURCE);
                 values.put(LauncherSettings.BaseLauncherColumns.ICON_PACKAGE,
                         iconResource.packageName);
                 values.put(LauncherSettings.BaseLauncherColumns.ICON_RESOURCE,
@@ -256,5 +272,23 @@
         mInstallProgress = progress;
         status |= FLAG_INSTALL_SESSION_ACTIVE;
     }
+
+    public boolean shouldUseLowResIcon() {
+        return usingLowResIcon && container >= 0 && rank >= FolderIcon.NUM_ITEMS_IN_PREVIEW;
+    }
+
+    public static ShortcutInfo fromActivityInfo(LauncherActivityInfoCompat info, Context context) {
+        final ShortcutInfo shortcut = new ShortcutInfo();
+        shortcut.user = info.getUser();
+        shortcut.title = info.getLabel().toString();
+        shortcut.contentDescription = UserManagerCompat.getInstance(context)
+                .getBadgedLabelForUser(info.getLabel(), info.getUser());
+        shortcut.customIcon = false;
+        shortcut.intent = AppInfo.makeLaunchIntent(context, info, info.getUser());
+        shortcut.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        shortcut.flags = AppInfo.initFlags(info);
+        shortcut.firstInstallTime = info.getFirstInstallTime();
+        return shortcut;
+    }
 }
 
diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java
index a879865..9d06f75 100644
--- a/src/com/android/launcher3/Stats.java
+++ b/src/com/android/launcher3/Stats.java
@@ -22,14 +22,8 @@
 import android.content.IntentFilter;
 import android.util.Log;
 
-import java.io.*;
-import java.util.ArrayList;
-
 public class Stats {
     private static final boolean DEBUG_BROADCASTS = false;
-    private static final String TAG = "Launcher3/Stats";
-
-    private static final boolean LOCAL_LAUNCH_LOG = true;
 
     public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH";
     public static final String EXTRA_INTENT = "intent";
@@ -38,54 +32,20 @@
     public static final String EXTRA_CELLX = "cellX";
     public static final String EXTRA_CELLY = "cellY";
 
-    private static final int LOG_VERSION = 1;
-    private static final int LOG_TAG_VERSION = 0x1;
-    private static final int LOG_TAG_LAUNCH = 0x1000;
-
-    private static final int STATS_VERSION = 1;
-    private static final int INITIAL_STATS_SIZE = 100;
-
-    // TODO: delayed/batched writes
-    private static final boolean FLUSH_IMMEDIATELY = true;
-
     private final Launcher mLauncher;
-
     private final String mLaunchBroadcastPermission;
 
-    DataOutputStream mLog;
-
-    ArrayList<String> mIntents;
-    ArrayList<Integer> mHistogram;
-
     public Stats(Launcher launcher) {
         mLauncher = launcher;
-
         mLaunchBroadcastPermission =
                 launcher.getResources().getString(R.string.receive_launch_broadcasts_permission);
 
-        loadStats();
-
-        if (LOCAL_LAUNCH_LOG) {
-            try {
-                mLog = new DataOutputStream(mLauncher.openFileOutput(
-                        LauncherFiles.LAUNCHES_LOG, Context.MODE_APPEND));
-                mLog.writeInt(LOG_TAG_VERSION);
-                mLog.writeInt(LOG_VERSION);
-            } catch (FileNotFoundException e) {
-                Log.e(TAG, "unable to create stats log: " + e);
-                mLog = null;
-            } catch (IOException e) {
-                Log.e(TAG, "unable to write to stats log: " + e);
-                mLog = null;
-            }
-        }
-
         if (DEBUG_BROADCASTS) {
             launcher.registerReceiver(
                     new BroadcastReceiver() {
                         @Override
                         public void onReceive(Context context, Intent intent) {
-                            android.util.Log.v("Stats", "got broadcast: " + intent + " for launched intent: "
+                            Log.v("Stats", "got broadcast: " + intent + " for launched intent: "
                                     + intent.getStringExtra(EXTRA_INTENT));
                         }
                     },
@@ -96,16 +56,6 @@
         }
     }
 
-    public void incrementLaunch(String intentStr) {
-        int pos = mIntents.indexOf(intentStr);
-        if (pos < 0) {
-            mIntents.add(intentStr);
-            mHistogram.add(1);
-        } else {
-            mHistogram.set(pos, mHistogram.get(pos) + 1);
-        }
-    }
-
     public void recordLaunch(Intent intent) {
         recordLaunch(intent, null);
     }
@@ -115,7 +65,6 @@
         intent.setSourceBounds(null);
 
         final String flat = intent.toUri(0);
-
         Intent broadcastIntent = new Intent(ACTION_LAUNCH).putExtra(EXTRA_INTENT, flat);
         if (shortcut != null) {
             broadcastIntent.putExtra(EXTRA_CONTAINER, shortcut.container)
@@ -124,94 +73,5 @@
                     .putExtra(EXTRA_CELLY, shortcut.cellY);
         }
         mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission);
-
-        incrementLaunch(flat);
-
-        if (FLUSH_IMMEDIATELY) {
-            saveStats();
-        }
-
-        if (LOCAL_LAUNCH_LOG && mLog != null) {
-            try {
-                mLog.writeInt(LOG_TAG_LAUNCH);
-                mLog.writeLong(System.currentTimeMillis());
-                if (shortcut == null) {
-                    mLog.writeShort(0);
-                    mLog.writeShort(0);
-                    mLog.writeShort(0);
-                    mLog.writeShort(0);
-                } else {
-                    mLog.writeShort((short) shortcut.container);
-                    mLog.writeShort((short) shortcut.screenId);
-                    mLog.writeShort((short) shortcut.cellX);
-                    mLog.writeShort((short) shortcut.cellY);
-                }
-                mLog.writeUTF(flat);
-                if (FLUSH_IMMEDIATELY) {
-                    mLog.flush(); // TODO: delayed writes
-                }
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-    }
-
-    private void saveStats() {
-        DataOutputStream stats = null;
-        try {
-            stats = new DataOutputStream(mLauncher.openFileOutput(
-                    LauncherFiles.STATS_LOG + ".tmp", Context.MODE_PRIVATE));
-            stats.writeInt(STATS_VERSION);
-            final int N = mHistogram.size();
-            stats.writeInt(N);
-            for (int i=0; i<N; i++) {
-                stats.writeUTF(mIntents.get(i));
-                stats.writeInt(mHistogram.get(i));
-            }
-            stats.close();
-            stats = null;
-            mLauncher.getFileStreamPath(LauncherFiles.STATS_LOG + ".tmp")
-                     .renameTo(mLauncher.getFileStreamPath(LauncherFiles.STATS_LOG));
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "unable to create stats data: " + e);
-        } catch (IOException e) {
-            Log.e(TAG, "unable to write to stats data: " + e);
-        } finally {
-            if (stats != null) {
-                try {
-                    stats.close();
-                } catch (IOException e) { }
-            }
-        }
-    }
-
-    private void loadStats() {
-        mIntents = new ArrayList<String>(INITIAL_STATS_SIZE);
-        mHistogram = new ArrayList<Integer>(INITIAL_STATS_SIZE);
-        DataInputStream stats = null;
-        try {
-            stats = new DataInputStream(mLauncher.openFileInput(LauncherFiles.STATS_LOG));
-            final int version = stats.readInt();
-            if (version == STATS_VERSION) {
-                final int N = stats.readInt();
-                for (int i=0; i<N; i++) {
-                    final String pkg = stats.readUTF();
-                    final int count = stats.readInt();
-                    mIntents.add(pkg);
-                    mHistogram.add(count);
-                }
-            }
-        } catch (FileNotFoundException e) {
-            // not a problem
-        } catch (IOException e) {
-            // more of a problem
-
-        } finally {
-            if (stats != null) {
-                try {
-                    stats.close();
-                } catch (IOException e) { }
-            }
-        }
     }
 }
diff --git a/src/com/android/launcher3/UninstallDropTarget.java b/src/com/android/launcher3/UninstallDropTarget.java
new file mode 100644
index 0000000..4a7fffe
--- /dev/null
+++ b/src/com/android/launcher3/UninstallDropTarget.java
@@ -0,0 +1,122 @@
+package com.android.launcher3;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.util.AttributeSet;
+import android.util.Pair;
+
+import com.android.launcher3.R;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.Thunk;
+
+public class UninstallDropTarget extends ButtonDropTarget {
+
+    public UninstallDropTarget(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public UninstallDropTarget(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        // Get the hover color
+        mHoverColor = getResources().getColor(R.color.delete_target_hover_tint);
+
+        setDrawable(R.drawable.uninstall_target_selector);
+    }
+
+    @Override
+    protected boolean supportsDrop(DragSource source, Object info) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            UserManager userManager = (UserManager)
+                    getContext().getSystemService(Context.USER_SERVICE);
+            Bundle restrictions = userManager.getUserRestrictions();
+            if (restrictions.getBoolean(UserManager.DISALLOW_APPS_CONTROL, false)
+                    || restrictions.getBoolean(UserManager.DISALLOW_UNINSTALL_APPS, false)) {
+                return false;
+            }
+        }
+
+        Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(info);
+        return componentInfo != null && (componentInfo.second & AppInfo.DOWNLOADED_FLAG) != 0;
+    }
+
+    /**
+     * @return the component name and flags if {@param info} is an AppInfo or an app shortcut.
+     */
+    private static Pair<ComponentName, Integer> getAppInfoFlags(Object item) {
+        if (item instanceof AppInfo) {
+            AppInfo info = (AppInfo) item;
+            return Pair.create(info.componentName, info.flags);
+        } else if (item instanceof ShortcutInfo) {
+            ShortcutInfo info = (ShortcutInfo) item;
+            ComponentName component = info.getTargetComponent();
+            if (info.itemType == LauncherSettings.BaseLauncherColumns.ITEM_TYPE_APPLICATION
+                    && component != null) {
+                return Pair.create(component, info.flags);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void onDrop(DragObject d) {
+        // Differ item deletion
+        if (d.dragSource instanceof UninstallSource) {
+            ((UninstallSource) d.dragSource).deferCompleteDropAfterUninstallActivity();
+        }
+        super.onDrop(d);
+    }
+
+    @Override
+    void completeDrop(final DragObject d) {
+        final Pair<ComponentName, Integer> componentInfo = getAppInfoFlags(d.dragInfo);
+        final UserHandleCompat user = ((ItemInfo) d.dragInfo).user;
+        if (mLauncher.startApplicationUninstallActivity(
+                componentInfo.first, componentInfo.second, user)) {
+
+            final Runnable checkIfUninstallWasSuccess = new Runnable() {
+                @Override
+                public void run() {
+                    String packageName = componentInfo.first.getPackageName();
+                    boolean uninstallSuccessful = !AllAppsList.packageHasActivities(
+                            getContext(), packageName, user);
+                    sendUninstallResult(d.dragSource, uninstallSuccessful);
+                }
+            };
+            mLauncher.addOnResumeCallback(checkIfUninstallWasSuccess);
+        } else {
+            sendUninstallResult(d.dragSource, false);
+        }
+    }
+
+    @Thunk void sendUninstallResult(DragSource target, boolean result) {
+        if (target instanceof UninstallSource) {
+            ((UninstallSource) target).onUninstallActivityReturned(result);
+        }
+    }
+
+    /**
+     * Interface defining an object that can provide uninstallable drag objects.
+     */
+    public static interface UninstallSource {
+
+        /**
+         * A pending uninstall operation was complete.
+         * @param result true if uninstall was successful, false otherwise.
+         */
+        void onUninstallActivityReturned(boolean result);
+
+        /**
+         * Indicates that an uninstall request are made and the actual result may come
+         * after some time.
+         */
+        void deferCompleteDropAfterUninstallActivity();
+    }
+}
diff --git a/src/com/android/launcher3/UninstallShortcutReceiver.java b/src/com/android/launcher3/UninstallShortcutReceiver.java
deleted file mode 100644
index c9d0bb5..0000000
--- a/src/com/android/launcher3/UninstallShortcutReceiver.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2008 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.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.net.Uri;
-import android.widget.Toast;
-
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Iterator;
-
-public class UninstallShortcutReceiver extends BroadcastReceiver {
-    private static final String ACTION_UNINSTALL_SHORTCUT =
-            "com.android.launcher.action.UNINSTALL_SHORTCUT";
-
-    // The set of shortcuts that are pending uninstall
-    private static ArrayList<PendingUninstallShortcutInfo> mUninstallQueue =
-            new ArrayList<PendingUninstallShortcutInfo>();
-
-    // Determines whether to defer uninstalling shortcuts immediately until
-    // disableAndFlushUninstallQueue() is called.
-    private static boolean mUseUninstallQueue = false;
-
-    private static class PendingUninstallShortcutInfo {
-        Intent data;
-
-        public PendingUninstallShortcutInfo(Intent rawData) {
-            data = rawData;
-        }
-    }
-
-    public void onReceive(Context context, Intent data) {
-        if (!ACTION_UNINSTALL_SHORTCUT.equals(data.getAction())) {
-            return;
-        }
-
-        PendingUninstallShortcutInfo info = new PendingUninstallShortcutInfo(data);
-        if (mUseUninstallQueue) {
-            mUninstallQueue.add(info);
-        } else {
-            processUninstallShortcut(context, info);
-        }
-    }
-
-    static void enableUninstallQueue() {
-        mUseUninstallQueue = true;
-    }
-
-    static void disableAndFlushUninstallQueue(Context context) {
-        mUseUninstallQueue = false;
-        Iterator<PendingUninstallShortcutInfo> iter = mUninstallQueue.iterator();
-        while (iter.hasNext()) {
-            processUninstallShortcut(context, iter.next());
-            iter.remove();
-        }
-    }
-
-    private static void processUninstallShortcut(Context context,
-            PendingUninstallShortcutInfo pendingInfo) {
-        final Intent data = pendingInfo.data;
-
-        LauncherAppState.setApplicationContext(context.getApplicationContext());
-        LauncherAppState app = LauncherAppState.getInstance();
-        synchronized (app) { // TODO: make removeShortcut internally threadsafe
-            removeShortcut(context, data);
-        }
-    }
-
-    private static void removeShortcut(Context context, Intent data) {
-        Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT);
-        String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME);
-        boolean duplicate = data.getBooleanExtra(Launcher.EXTRA_SHORTCUT_DUPLICATE, true);
-
-        if (intent != null && name != null) {
-            final ContentResolver cr = context.getContentResolver();
-            Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI,
-                new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.INTENT },
-                LauncherSettings.Favorites.TITLE + "=?", new String[] { name }, null);
-
-            final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
-            final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
-
-            boolean changed = false;
-
-            try {
-                while (c.moveToNext()) {
-                    try {
-                        String intentStr = c.getString(intentIndex);
-                        if (intentStr != null
-                                && intent.filterEquals(Intent.parseUri(intentStr, 0))) {
-                            final long id = c.getLong(idIndex);
-                            final Uri uri = LauncherSettings.Favorites.getContentUri(id, false);
-                            cr.delete(uri, null, null);
-                            changed = true;
-                            if (!duplicate) {
-                                break;
-                            }
-                        }
-                    } catch (URISyntaxException e) {
-                        // Ignore
-                    }
-                }
-            } finally {
-                c.close();
-            }
-
-            if (changed) {
-                cr.notifyChange(LauncherSettings.Favorites.CONTENT_URI, null);
-                Toast.makeText(context, context.getString(R.string.shortcut_uninstalled, name),
-                        Toast.LENGTH_SHORT).show();
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/UserInitializeReceiver.java b/src/com/android/launcher3/UserInitializeReceiver.java
deleted file mode 100644
index d8e17b1..0000000
--- a/src/com/android/launcher3/UserInitializeReceiver.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2012 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.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * Takes care of setting initial wallpaper for a user, by selecting the
- * first wallpaper that is not in use by another user.
- */
-public class UserInitializeReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        // TODO: initial wallpaper now that wallpapers are owned by another app
-    }
-}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 1a9b9a1..22677c8 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -31,7 +31,9 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.database.Cursor;
 import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Matrix;
@@ -48,6 +50,8 @@
 import android.view.View;
 import android.widget.Toast;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Comparator;
 
@@ -112,6 +116,15 @@
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
     }
 
+    static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) {
+        byte[] data = c.getBlob(iconIndex);
+        try {
+            return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     /**
      * Returns a bitmap suitable for the all apps view. If the package or the resource do not
      * exist, it returns null.
@@ -544,6 +557,25 @@
         return defaultWidgetForSearchPackage;
     }
 
+    /**
+     * Compresses the bitmap to a byte array for serialization.
+     */
+    public static byte[] flattenBitmap(Bitmap bitmap) {
+        // Try go guesstimate how much space the icon will take when serialized
+        // to avoid unnecessary allocations/copies during the write.
+        int size = bitmap.getWidth() * bitmap.getHeight() * 4;
+        ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+        try {
+            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
+            out.flush();
+            out.close();
+            return out.toByteArray();
+        } catch (IOException e) {
+            Log.w(TAG, "Could not write bitmap");
+            return null;
+        }
+    }
+
     public static final Comparator<ItemInfo> RANK_COMPARATOR = new Comparator<ItemInfo>() {
 
         @Override
diff --git a/src/com/android/launcher3/WeightWatcher.java b/src/com/android/launcher3/WeightWatcher.java
index 70b8afe..7568479 100644
--- a/src/com/android/launcher3/WeightWatcher.java
+++ b/src/com/android/launcher3/WeightWatcher.java
@@ -34,6 +34,8 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.launcher3.util.Thunk;
+
 public class WeightWatcher extends LinearLayout {
     private static final int RAM_GRAPH_RSS_COLOR = 0xFF990000;
     private static final int RAM_GRAPH_PSS_COLOR = 0xFF99CC00;
@@ -81,7 +83,7 @@
             }
         }
     };
-    private MemoryTracker mMemoryService;
+    @Thunk MemoryTracker mMemoryService;
 
     public WeightWatcher(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -134,7 +136,7 @@
         GraphView mRamGraph;
         TextView mText;
         int mPid;
-        private MemoryTracker.ProcessMemInfo mMemInfo;
+        @Thunk MemoryTracker.ProcessMemInfo mMemInfo;
 
         public ProcessWatcher(Context context) {
             this(context, null);
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index d963f2d..5c3ed92 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -1,524 +1,361 @@
 package com.android.launcher3;
 
-import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteDiskIOException;
 import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteReadOnlyDatabaseException;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
-import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.Rect;
-import android.graphics.Shader;
+import android.graphics.RectF;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.util.Log;
-import com.android.launcher3.compat.AppWidgetManagerCompat;
+import android.util.LongSparseArray;
 
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.lang.ref.SoftReference;
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.widget.WidgetCell;
+
 import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
+import java.util.Set;
+import java.util.WeakHashMap;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 
 public class WidgetPreviewLoader {
 
-    private static abstract class SoftReferenceThreadLocal<T> {
-        private ThreadLocal<SoftReference<T>> mThreadLocal;
-        public SoftReferenceThreadLocal() {
-            mThreadLocal = new ThreadLocal<SoftReference<T>>();
-        }
-
-        abstract T initialValue();
-
-        public void set(T t) {
-            mThreadLocal.set(new SoftReference<T>(t));
-        }
-
-        public T get() {
-            SoftReference<T> reference = mThreadLocal.get();
-            T obj;
-            if (reference == null) {
-                obj = initialValue();
-                mThreadLocal.set(new SoftReference<T>(obj));
-                return obj;
-            } else {
-                obj = reference.get();
-                if (obj == null) {
-                    obj = initialValue();
-                    mThreadLocal.set(new SoftReference<T>(obj));
-                }
-                return obj;
-            }
-        }
-    }
-
-    private static class CanvasCache extends SoftReferenceThreadLocal<Canvas> {
-        @Override
-        protected Canvas initialValue() {
-            return new Canvas();
-        }
-    }
-
-    private static class PaintCache extends SoftReferenceThreadLocal<Paint> {
-        @Override
-        protected Paint initialValue() {
-            return null;
-        }
-    }
-
-    private static class BitmapCache extends SoftReferenceThreadLocal<Bitmap> {
-        @Override
-        protected Bitmap initialValue() {
-            return null;
-        }
-    }
-
-    private static class RectCache extends SoftReferenceThreadLocal<Rect> {
-        @Override
-        protected Rect initialValue() {
-            return new Rect();
-        }
-    }
-
-    private static class BitmapFactoryOptionsCache extends
-            SoftReferenceThreadLocal<BitmapFactory.Options> {
-        @Override
-        protected BitmapFactory.Options initialValue() {
-            return new BitmapFactory.Options();
-        }
-    }
-
     private static final String TAG = "WidgetPreviewLoader";
-    private static final String ANDROID_INCREMENTAL_VERSION_NAME_KEY = "android.incremental.version";
+    private static final boolean DEBUG = false;
 
     private static final float WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE = 0.25f;
-    private static final HashSet<String> sInvalidPackages = new HashSet<String>();
 
-    // Used for drawing shortcut previews
-    private final BitmapCache mCachedShortcutPreviewBitmap = new BitmapCache();
-    private final PaintCache mCachedShortcutPreviewPaint = new PaintCache();
-    private final CanvasCache mCachedShortcutPreviewCanvas = new CanvasCache();
-
-    // Used for drawing widget previews
-    private final CanvasCache mCachedAppWidgetPreviewCanvas = new CanvasCache();
-    private final RectCache mCachedAppWidgetPreviewSrcRect = new RectCache();
-    private final RectCache mCachedAppWidgetPreviewDestRect = new RectCache();
-    private final PaintCache mCachedAppWidgetPreviewPaint = new PaintCache();
-    private final PaintCache mDefaultAppWidgetPreviewPaint = new PaintCache();
-    private final BitmapFactoryOptionsCache mCachedBitmapFactoryOptions = new BitmapFactoryOptionsCache();
-
-    private final HashMap<String, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<String, WeakReference<Bitmap>>();
-    private final ArrayList<SoftReference<Bitmap>> mUnusedBitmaps = new ArrayList<SoftReference<Bitmap>>();
+    private final HashMap<String, long[]> mPackageVersions = new HashMap<>();
+    private final HashMap<WidgetCacheKey, WeakReference<Bitmap>> mLoadedPreviews = new HashMap<>();
+    private Set<Bitmap> mUnusedBitmaps = Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
 
     private final Context mContext;
-    private final int mAppIconSize;
     private final IconCache mIconCache;
+    private final UserManagerCompat mUserManager;
     private final AppWidgetManagerCompat mManager;
-
-    private int mPreviewBitmapWidth;
-    private int mPreviewBitmapHeight;
-    private String mSize;
-    private PagedViewCellLayout mWidgetSpacingLayout;
-
-    private String mCachedSelectQuery;
-
-
-    private CacheDb mDb;
+    private final CacheDb mDb;
 
     private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
 
-    public WidgetPreviewLoader(Context context) {
-        LauncherAppState app = LauncherAppState.getInstance();
-        DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
-
+    public WidgetPreviewLoader(Context context, IconCache iconCache) {
         mContext = context;
-        mAppIconSize = grid.iconSizePx;
-        mIconCache = app.getIconCache();
+        mIconCache = iconCache;
         mManager = AppWidgetManagerCompat.getInstance(context);
-
-        mDb = app.getWidgetPreviewCacheDb();
-
-        SharedPreferences sp = context.getSharedPreferences(
-                LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
-        final String lastVersionName = sp.getString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, null);
-        final String versionName = android.os.Build.VERSION.INCREMENTAL;
-        final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
-        if (!versionName.equals(lastVersionName)) {
-            try {
-                // clear all the previews whenever the system version changes, to ensure that
-                // previews are up-to-date for any apps that might have been updated with the system
-                clearDb();
-            } catch (SQLiteReadOnlyDatabaseException e) {
-                if (isLollipopOrGreater) {
-                    // Workaround for Bug. 18554839, if we fail to clear the db due to the read-only
-                    // issue, then ignore this error and leave the old previews
-                } else {
-                    throw e;
-                }
-            } finally {
-                SharedPreferences.Editor editor = sp.edit();
-                editor.putString(ANDROID_INCREMENTAL_VERSION_NAME_KEY, versionName);
-                editor.commit();
-            }
-        }
+        mUserManager = UserManagerCompat.getInstance(context);
+        mDb = new CacheDb(context);
     }
 
-    public void recreateDb() {
-        LauncherAppState app = LauncherAppState.getInstance();
-        app.recreateWidgetPreviewDb();
-        mDb = app.getWidgetPreviewCacheDb();
-    }
+    /**
+     * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
+     * called on UI thread
+     *
+     * @param o either {@link LauncherAppWidgetProviderInfo} or {@link ResolveInfo}
+     * @param immediateResult A bitmap array of size 1. If the result is already cached, it is
+     * set to the final result.
+     * @return a request id which can be used to cancel the request.
+     */
+    public PreviewLoadRequest getPreview(final Object o, int previewWidth, int previewHeight,
+            WidgetCell caller, Bitmap[] immediateResult) {
+        String size = previewWidth + "x" + previewHeight;
+        WidgetCacheKey key = getObjectKey(o, size);
 
-    public void setPreviewSize(int previewWidth, int previewHeight,
-            PagedViewCellLayout widgetSpacingLayout) {
-        mPreviewBitmapWidth = previewWidth;
-        mPreviewBitmapHeight = previewHeight;
-        mSize = previewWidth + "x" + previewHeight;
-        mWidgetSpacingLayout = widgetSpacingLayout;
-    }
-
-    public Bitmap getPreview(final Object o) {
-        final String name = getObjectName(o);
-        final String packageName = getObjectPackage(o);
-        // check if the package is valid
-        synchronized(sInvalidPackages) {
-            boolean packageValid = !sInvalidPackages.contains(packageName);
-            if (!packageValid) {
-                return null;
-            }
-        }
-        synchronized(mLoadedPreviews) {
-            // check if it exists in our existing cache
-            if (mLoadedPreviews.containsKey(name)) {
-                WeakReference<Bitmap> bitmapReference = mLoadedPreviews.get(name);
-                Bitmap bitmap = bitmapReference.get();
-                if (bitmap != null) {
-                    return bitmap;
-                }
-            }
-        }
-
-        Bitmap unusedBitmap = null;
-        synchronized(mUnusedBitmaps) {
-            // not in cache; we need to load it from the db
-            while (unusedBitmap == null && mUnusedBitmaps.size() > 0) {
-                Bitmap candidate = mUnusedBitmaps.remove(0).get();
-                if (candidate != null && candidate.isMutable() &&
-                        candidate.getWidth() == mPreviewBitmapWidth &&
-                        candidate.getHeight() == mPreviewBitmapHeight) {
-                    unusedBitmap = candidate;
-                }
-            }
-            if (unusedBitmap != null) {
-                final Canvas c = mCachedAppWidgetPreviewCanvas.get();
-                c.setBitmap(unusedBitmap);
-                c.drawColor(0, PorterDuff.Mode.CLEAR);
-                c.setBitmap(null);
-            }
-        }
-
-        if (unusedBitmap == null) {
-            unusedBitmap = Bitmap.createBitmap(mPreviewBitmapWidth, mPreviewBitmapHeight,
-                    Bitmap.Config.ARGB_8888);
-        }
-        Bitmap preview = readFromDb(name, unusedBitmap);
-
-        if (preview != null) {
-            synchronized(mLoadedPreviews) {
-                mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview));
-            }
-            return preview;
-        } else {
-            // it's not in the db... we need to generate it
-            final Bitmap generatedPreview = generatePreview(o, unusedBitmap);
-            preview = generatedPreview;
-            if (preview != unusedBitmap) {
-                throw new RuntimeException("generatePreview is not recycling the bitmap " + o);
-            }
-
-            synchronized(mLoadedPreviews) {
-                mLoadedPreviews.put(name, new WeakReference<Bitmap>(preview));
-            }
-
-            // write to db on a thread pool... this can be done lazily and improves the performance
-            // of the first time widget previews are loaded
-            new AsyncTask<Void, Void, Void>() {
-                public Void doInBackground(Void ... args) {
-                    writeToDb(o, generatedPreview);
-                    return null;
-                }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
-
-            return preview;
-        }
-    }
-
-    public void recycleBitmap(Object o, Bitmap bitmapToRecycle) {
-        String name = getObjectName(o);
+        // Check if we have the preview loaded or not.
         synchronized (mLoadedPreviews) {
-            if (mLoadedPreviews.containsKey(name)) {
-                Bitmap b = mLoadedPreviews.get(name).get();
-                if (b == bitmapToRecycle) {
-                    mLoadedPreviews.remove(name);
-                    if (bitmapToRecycle.isMutable()) {
-                        synchronized (mUnusedBitmaps) {
-                            mUnusedBitmaps.add(new SoftReference<Bitmap>(b));
-                        }
-                    }
-                } else {
-                    throw new RuntimeException("Bitmap passed in doesn't match up");
-                }
+            WeakReference<Bitmap> ref = mLoadedPreviews.get(key);
+            if (ref != null && ref.get() != null) {
+                immediateResult[0] = ref.get();
+                return new PreviewLoadRequest(null, key);
             }
         }
+
+        PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller);
+        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        return new PreviewLoadRequest(task, key);
     }
 
-    static class CacheDb extends SQLiteOpenHelper {
-        final static int DB_VERSION = 2;
-        final static String TABLE_NAME = "shortcut_and_widget_previews";
-        final static String COLUMN_NAME = "name";
-        final static String COLUMN_SIZE = "size";
-        final static String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
-        Context mContext;
+    /**
+     * The DB holds the generated previews for various components. Previews can also have different
+     * sizes (landscape vs portrait).
+     */
+    private static class CacheDb extends SQLiteOpenHelper {
+        private static final int DB_VERSION = 3;
+
+        private static final String TABLE_NAME = "shortcut_and_widget_previews";
+        private static final String COLUMN_COMPONENT = "componentName";
+        private static final String COLUMN_USER = "profileId";
+        private static final String COLUMN_SIZE = "size";
+        private static final String COLUMN_PACKAGE = "packageName";
+        private static final String COLUMN_LAST_UPDATED = "lastUpdated";
+        private static final String COLUMN_VERSION = "version";
+        private static final String COLUMN_PREVIEW_BITMAP = "preview_bitmap";
 
         public CacheDb(Context context) {
-            super(context, new File(context.getCacheDir(),
-                    LauncherFiles.WIDGET_PREVIEWS_DB).getPath(), null, DB_VERSION);
-            // Store the context for later use
-            mContext = context;
+            super(context, LauncherFiles.WIDGET_PREVIEWS_DB, null, DB_VERSION);
         }
 
         @Override
         public void onCreate(SQLiteDatabase database) {
             database.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
-                    COLUMN_NAME + " TEXT NOT NULL, " +
+                    COLUMN_COMPONENT + " TEXT NOT NULL, " +
+                    COLUMN_USER + " INTEGER NOT NULL, " +
                     COLUMN_SIZE + " TEXT NOT NULL, " +
-                    COLUMN_PREVIEW_BITMAP + " BLOB NOT NULL, " +
-                    "PRIMARY KEY (" + COLUMN_NAME + ", " + COLUMN_SIZE + ") " +
+                    COLUMN_PACKAGE + " TEXT NOT NULL, " +
+                    COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, " +
+                    COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, " +
+                    COLUMN_PREVIEW_BITMAP + " BLOB, " +
+                    "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ", " + COLUMN_SIZE + ") " +
                     ");");
         }
 
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             if (oldVersion != newVersion) {
-                // Delete all the records; they'll be repopulated as this is a cache
-                db.execSQL("DELETE FROM " + TABLE_NAME);
+                clearDB(db);
             }
         }
+
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        private void clearDB(SQLiteDatabase db) {
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+            onCreate(db);
+        }
     }
 
-    private static final String WIDGET_PREFIX = "Widget:";
-    private static final String SHORTCUT_PREFIX = "Shortcut:";
-
-    private static String getObjectName(Object o) {
+    private WidgetCacheKey getObjectKey(Object o, String size) {
         // should cache the string builder
-        StringBuilder sb = new StringBuilder();
-        String output;
-        if (o instanceof AppWidgetProviderInfo) {
-            sb.append(WIDGET_PREFIX);
-            sb.append(((AppWidgetProviderInfo) o).toString());
-            output = sb.toString();
-            sb.setLength(0);
-        } else {
-            sb.append(SHORTCUT_PREFIX);
-            ResolveInfo info = (ResolveInfo) o;
-            sb.append(new ComponentName(info.activityInfo.packageName,
-                    info.activityInfo.name).flattenToString());
-            output = sb.toString();
-            sb.setLength(0);
-        }
-        return output;
-    }
-
-    private String getObjectPackage(Object o) {
-        if (o instanceof AppWidgetProviderInfo) {
-            return ((AppWidgetProviderInfo) o).provider.getPackageName();
+        if (o instanceof LauncherAppWidgetProviderInfo) {
+            LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) o;
+            return new WidgetCacheKey(info.provider, mManager.getUser(info), size);
         } else {
             ResolveInfo info = (ResolveInfo) o;
-            return info.activityInfo.packageName;
+            return new WidgetCacheKey(
+                    new ComponentName(info.activityInfo.packageName, info.activityInfo.name),
+                    UserHandleCompat.myUserHandle(), size);
         }
     }
 
-    private void writeToDb(Object o, Bitmap preview) {
-        String name = getObjectName(o);
-        SQLiteDatabase db = mDb.getWritableDatabase();
+    @Thunk void writeToDb(WidgetCacheKey key, long[] versions, Bitmap preview) {
         ContentValues values = new ContentValues();
+        values.put(CacheDb.COLUMN_COMPONENT, key.componentName.flattenToShortString());
+        values.put(CacheDb.COLUMN_USER, mUserManager.getSerialNumberForUser(key.user));
+        values.put(CacheDb.COLUMN_SIZE, key.size);
+        values.put(CacheDb.COLUMN_PACKAGE, key.componentName.getPackageName());
+        values.put(CacheDb.COLUMN_VERSION, versions[0]);
+        values.put(CacheDb.COLUMN_LAST_UPDATED, versions[1]);
+        values.put(CacheDb.COLUMN_PREVIEW_BITMAP, Utilities.flattenBitmap(preview));
 
-        values.put(CacheDb.COLUMN_NAME, name);
-        ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        preview.compress(Bitmap.CompressFormat.PNG, 100, stream);
-        values.put(CacheDb.COLUMN_PREVIEW_BITMAP, stream.toByteArray());
-        values.put(CacheDb.COLUMN_SIZE, mSize);
         try {
-            db.insert(CacheDb.TABLE_NAME, null, values);
-        } catch (SQLiteDiskIOException e) {
-            recreateDb();
-        } catch (SQLiteCantOpenDatabaseException e) {
-            dumpOpenFiles();
-            throw e;
+            mDb.getWritableDatabase().insertWithOnConflict(CacheDb.TABLE_NAME, null, values,
+                    SQLiteDatabase.CONFLICT_REPLACE);
+        } catch (SQLException e) {
+            Log.e(TAG, "Error saving image to DB", e);
         }
     }
 
-    private void clearDb() {
-        SQLiteDatabase db = mDb.getWritableDatabase();
-        // Delete everything
+    public void removePackage(String packageName, UserHandleCompat user) {
+        removePackage(packageName, user, mUserManager.getSerialNumberForUser(user));
+    }
+
+    private void removePackage(String packageName, UserHandleCompat user, long userSerial) {
+        synchronized(mPackageVersions) {
+            mPackageVersions.remove(packageName);
+        }
+
+        synchronized (mLoadedPreviews) {
+            Set<WidgetCacheKey> keysToRemove = new HashSet<>();
+            for (WidgetCacheKey key : mLoadedPreviews.keySet()) {
+                if (key.componentName.getPackageName().equals(packageName) && key.user.equals(user)) {
+                    keysToRemove.add(key);
+                }
+            }
+
+            for (WidgetCacheKey key : keysToRemove) {
+                WeakReference<Bitmap> req = mLoadedPreviews.remove(key);
+                if (req != null && req.get() != null) {
+                    mUnusedBitmaps.add(req.get());
+                }
+            }
+        }
+
         try {
-            db.delete(CacheDb.TABLE_NAME, null, null);
-        } catch (SQLiteDiskIOException e) {
-        } catch (SQLiteCantOpenDatabaseException e) {
-            dumpOpenFiles();
-            throw e;
+            mDb.getWritableDatabase().delete(CacheDb.TABLE_NAME,
+                    CacheDb.COLUMN_PACKAGE + " = ? AND " + CacheDb.COLUMN_USER + " = ?",
+                    new String[] {packageName, Long.toString(userSerial)});
+        } catch (SQLException e) {
+            Log.e(TAG, "Unable to delete items from DB", e);
         }
     }
 
-    public static void removePackageFromDb(final CacheDb cacheDb, final String packageName) {
-        synchronized(sInvalidPackages) {
-            sInvalidPackages.add(packageName);
+    /**
+     * Updates the persistent DB:
+     *   1. Any preview generated for an old package version is removed
+     *   2. Any preview for an absent package is removed
+     * This ensures that we remove entries for packages which changed while the launcher was dead.
+     */
+    public void removeObsoletePreviews() {
+        LongSparseArray<UserHandleCompat> userIdCache = new LongSparseArray<>();
+        LongSparseArray<HashSet<String>> validPackages = new LongSparseArray<>();
+
+        for (Object obj : LauncherModel.getSortedWidgetsAndShortcuts(mContext, false)) {
+            final UserHandleCompat user;
+            final String pkg;
+            if (obj instanceof ResolveInfo) {
+                user = UserHandleCompat.myUserHandle();
+                pkg = ((ResolveInfo) obj).activityInfo.packageName;
+            } else {
+                LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) obj;
+                user = mManager.getUser(info);
+                pkg = info.provider.getPackageName();
+            }
+
+            int userIdIndex = userIdCache.indexOfValue(user);
+            final long userId;
+            if (userIdIndex < 0) {
+                userId = mUserManager.getSerialNumberForUser(user);
+                userIdCache.put(userId, user);
+            } else {
+                userId = userIdCache.keyAt(userIdIndex);
+            }
+
+            HashSet<String> packages = validPackages.get(userId);
+            if (packages == null) {
+                packages = new HashSet<>();
+                validPackages.put(userId, packages);
+            }
+            packages.add(pkg);
         }
-        new AsyncTask<Void, Void, Void>() {
-            public Void doInBackground(Void ... args) {
-                SQLiteDatabase db = cacheDb.getWritableDatabase();
+
+        LongSparseArray<HashSet<String>> packagesToDelete = new LongSparseArray<>();
+        Cursor c = null;
+        try {
+            c = mDb.getReadableDatabase().query(CacheDb.TABLE_NAME,
+                    new String[] {CacheDb.COLUMN_USER, CacheDb.COLUMN_PACKAGE,
+                        CacheDb.COLUMN_LAST_UPDATED, CacheDb.COLUMN_VERSION},
+                    null, null, null, null, null);
+            while (c.moveToNext()) {
+                long userId = c.getLong(0);
+                String pkg = c.getString(1);
+                long lastUpdated = c.getLong(2);
+                long version = c.getLong(3);
+
+                HashSet<String> packages = validPackages.get(userId);
+                if (packages != null && packages.contains(pkg)) {
+                    long[] versions = getPackageVersion(pkg);
+                    if (versions[0] == version && versions[1] == lastUpdated) {
+                        // Every thing checks out
+                        continue;
+                    }
+                }
+
+                // We need to delete this package.
+                packages = packagesToDelete.get(userId);
+                if (packages == null) {
+                    packages = new HashSet<>();
+                    packagesToDelete.put(userId, packages);
+                }
+                packages.add(pkg);
+            }
+
+            for (int i = 0; i < packagesToDelete.size(); i++) {
+                long userId = packagesToDelete.keyAt(i);
+                UserHandleCompat user = mUserManager.getUserForSerialNumber(userId);
+                for (String pkg : packagesToDelete.valueAt(i)) {
+                    removePackage(pkg, user, userId);
+                }
+            }
+        } catch (SQLException e) {
+            Log.e(TAG, "Error updatating widget previews", e);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+    }
+
+    private Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle) {
+        Cursor cursor = null;
+        try {
+            cursor = mDb.getReadableDatabase().query(
+                    CacheDb.TABLE_NAME,
+                    new String[] { CacheDb.COLUMN_PREVIEW_BITMAP },
+                    CacheDb.COLUMN_COMPONENT + " = ? AND " + CacheDb.COLUMN_USER + " = ? AND " + CacheDb.COLUMN_SIZE + " = ?",
+                    new String[] {
+                            key.componentName.flattenToString(),
+                            Long.toString(mUserManager.getSerialNumberForUser(key.user)),
+                            key.size
+                    },
+                    null, null, null);
+            if (cursor.moveToNext()) {
+                byte[] blob = cursor.getBlob(0);
+                BitmapFactory.Options opts = new BitmapFactory.Options();
+                opts.inBitmap = recycle;
                 try {
-                    db.delete(CacheDb.TABLE_NAME,
-                            CacheDb.COLUMN_NAME + " LIKE ? OR " +
-                            CacheDb.COLUMN_NAME + " LIKE ?", // SELECT query
-                            new String[] {
-                                    WIDGET_PREFIX + packageName + "/%",
-                                    SHORTCUT_PREFIX + packageName + "/%"
-                            } // args to SELECT query
-                    );
-                } catch (SQLiteDiskIOException e) {
-                } catch (SQLiteCantOpenDatabaseException e) {
-                    dumpOpenFiles();
-                    throw e;
+                    return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
+                } catch (Exception e) {
+                    return null;
                 }
-                synchronized(sInvalidPackages) {
-                    sInvalidPackages.remove(packageName);
-                }
-                return null;
             }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
+        } catch (SQLException e) {
+            Log.w(TAG, "Error loading preview from DB", e);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return null;
     }
 
-    private static void removeItemFromDb(final CacheDb cacheDb, final String objectName) {
-        new AsyncTask<Void, Void, Void>() {
-            public Void doInBackground(Void ... args) {
-                SQLiteDatabase db = cacheDb.getWritableDatabase();
-                try {
-                    db.delete(CacheDb.TABLE_NAME,
-                            CacheDb.COLUMN_NAME + " = ? ", // SELECT query
-                            new String[] { objectName }); // args to SELECT query
-                } catch (SQLiteDiskIOException e) {
-                } catch (SQLiteCantOpenDatabaseException e) {
-                    dumpOpenFiles();
-                    throw e;
-                }
-                return null;
-            }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
-    }
-
-    private Bitmap readFromDb(String name, Bitmap b) {
-        if (mCachedSelectQuery == null) {
-            mCachedSelectQuery = CacheDb.COLUMN_NAME + " = ? AND " +
-                    CacheDb.COLUMN_SIZE + " = ?";
-        }
-        SQLiteDatabase db = mDb.getReadableDatabase();
-        Cursor result;
-        try {
-            result = db.query(CacheDb.TABLE_NAME,
-                    new String[] { CacheDb.COLUMN_PREVIEW_BITMAP }, // cols to return
-                    mCachedSelectQuery, // select query
-                    new String[] { name, mSize }, // args to select query
-                    null,
-                    null,
-                    null,
-                    null);
-        } catch (SQLiteDiskIOException e) {
-            recreateDb();
-            return null;
-        } catch (SQLiteCantOpenDatabaseException e) {
-            dumpOpenFiles();
-            throw e;
-        }
-        if (result.getCount() > 0) {
-            result.moveToFirst();
-            byte[] blob = result.getBlob(0);
-            result.close();
-            final BitmapFactory.Options opts = mCachedBitmapFactoryOptions.get();
-            opts.inBitmap = b;
-            opts.inSampleSize = 1;
-            try {
-                return BitmapFactory.decodeByteArray(blob, 0, blob.length, opts);
-            } catch (IllegalArgumentException e) {
-                removeItemFromDb(mDb, name);
-                return null;
-            }
-        } else {
-            result.close();
-            return null;
-        }
-    }
-
-    private Bitmap generatePreview(Object info, Bitmap preview) {
-        if (preview != null &&
-                (preview.getWidth() != mPreviewBitmapWidth ||
-                preview.getHeight() != mPreviewBitmapHeight)) {
-            throw new RuntimeException("Improperly sized bitmap passed as argument");
-        }
+    private Bitmap generatePreview(Object info, Bitmap recycle, int previewWidth, int previewHeight) {
         if (info instanceof LauncherAppWidgetProviderInfo) {
-            return generateWidgetPreview((LauncherAppWidgetProviderInfo) info, preview);
+            return generateWidgetPreview((LauncherAppWidgetProviderInfo) info, previewWidth, recycle);
         } else {
             return generateShortcutPreview(
-                    (ResolveInfo) info, mPreviewBitmapWidth, mPreviewBitmapHeight, preview);
+                    (ResolveInfo) info, previewWidth, previewHeight, recycle);
         }
     }
 
-    public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, Bitmap preview) {
-        int maxWidth = maxWidthForWidgetPreview(info.spanX);
-        int maxHeight = maxHeightForWidgetPreview(info.spanY);
-        return generateWidgetPreview(info, info.spanX, info.spanY, maxWidth,
-                maxHeight, preview, null);
+    public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
+            int previewWidth, Bitmap preview) {
+        int maxWidth = Math.min(previewWidth, info.spanX
+                * LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile().cellWidthPx);
+        return generateWidgetPreview(info, maxWidth, preview, null);
     }
 
-    public int maxWidthForWidgetPreview(int spanX) {
-        return Math.min(mPreviewBitmapWidth,
-                mWidgetSpacingLayout.estimateCellWidth(spanX));
-    }
-
-    public int maxHeightForWidgetPreview(int spanY) {
-        return Math.min(mPreviewBitmapHeight,
-                mWidgetSpacingLayout.estimateCellHeight(spanY));
-    }
-
-    public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, int cellHSpan, int cellVSpan,
-            int maxPreviewWidth, int maxPreviewHeight, Bitmap preview, int[] preScaledWidthOut) {
+    public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
+            int maxPreviewWidth, Bitmap preview, int[] preScaledWidthOut) {
         // Load the preview image if possible
         if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
-        if (maxPreviewHeight < 0) maxPreviewHeight = Integer.MAX_VALUE;
 
         Drawable drawable = null;
         if (info.previewImage != 0) {
@@ -531,61 +368,23 @@
             }
         }
 
+        final boolean widgetPreviewExists = (drawable != null);
+        final int spanX = info.spanX < 1 ? 1 : info.spanX;
+        final int spanY = info.spanY < 1 ? 1 : info.spanY;
+
         int previewWidth;
         int previewHeight;
-        Bitmap defaultPreview = null;
-        boolean widgetPreviewExists = (drawable != null);
+        Bitmap tileBitmap = null;
+
         if (widgetPreviewExists) {
             previewWidth = drawable.getIntrinsicWidth();
             previewHeight = drawable.getIntrinsicHeight();
         } else {
             // Generate a preview image if we couldn't load one
-            if (cellHSpan < 1) cellHSpan = 1;
-            if (cellVSpan < 1) cellVSpan = 1;
-
-            // This Drawable is not directly drawn, so there's no need to mutate it.
-            BitmapDrawable previewDrawable = (BitmapDrawable) mContext.getResources()
-                    .getDrawable(R.drawable.widget_tile);
-            final int previewDrawableWidth = previewDrawable
-                    .getIntrinsicWidth();
-            final int previewDrawableHeight = previewDrawable
-                    .getIntrinsicHeight();
-            previewWidth = previewDrawableWidth * cellHSpan;
-            previewHeight = previewDrawableHeight * cellVSpan;
-
-            defaultPreview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
-            final Canvas c = mCachedAppWidgetPreviewCanvas.get();
-            c.setBitmap(defaultPreview);
-            Paint p = mDefaultAppWidgetPreviewPaint.get();
-            if (p == null) {
-                p = new Paint();
-                p.setShader(new BitmapShader(previewDrawable.getBitmap(),
-                        Shader.TileMode.REPEAT, Shader.TileMode.REPEAT));
-                mDefaultAppWidgetPreviewPaint.set(p);
-            }
-            final Rect dest = mCachedAppWidgetPreviewDestRect.get();
-            dest.set(0, 0, previewWidth, previewHeight);
-            c.drawRect(dest, p);
-            c.setBitmap(null);
-
-            // Draw the icon in the top left corner
-            int minOffset = (int) (mAppIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE);
-            int smallestSide = Math.min(previewWidth, previewHeight);
-            float iconScale = Math.min((float) smallestSide
-                    / (mAppIconSize + 2 * minOffset), 1f);
-
-            try {
-                Drawable icon = mManager.loadIcon(info, mIconCache);
-                if (icon != null) {
-                    int hoffset = (int) ((previewDrawableWidth - mAppIconSize * iconScale) / 2);
-                    int yoffset = (int) ((previewDrawableHeight - mAppIconSize * iconScale) / 2);
-                    icon = mutateOnMainThread(icon);
-                    renderDrawableToBitmap(icon, defaultPreview, hoffset,
-                            yoffset, (int) (mAppIconSize * iconScale),
-                            (int) (mAppIconSize * iconScale));
-                }
-            } catch (Resources.NotFoundException e) {
-            }
+            tileBitmap = ((BitmapDrawable) mContext.getResources().getDrawable(
+                    R.drawable.widget_tile)).getBitmap();
+            previewWidth = tileBitmap.getWidth() * spanX;
+            previewHeight = tileBitmap.getHeight() * spanY;
         }
 
         // Scale to fit width only - let the widget preview be clipped in the
@@ -603,30 +402,60 @@
         }
 
         // If a bitmap is passed in, we use it; otherwise, we create a bitmap of the right size
+        final Canvas c = new Canvas();
         if (preview == null) {
             preview = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
+            c.setBitmap(preview);
+        } else {
+            // Reusing bitmap. Clear it.
+            c.setBitmap(preview);
+            c.drawColor(0, PorterDuff.Mode.CLEAR);
         }
 
         // Draw the scaled preview into the final bitmap
         int x = (preview.getWidth() - previewWidth) / 2;
         if (widgetPreviewExists) {
-            renderDrawableToBitmap(drawable, preview, x, 0, previewWidth,
-                    previewHeight);
+            drawable.setBounds(x, 0, x + previewWidth, previewHeight);
+            drawable.draw(c);
         } else {
-            final Canvas c = mCachedAppWidgetPreviewCanvas.get();
-            final Rect src = mCachedAppWidgetPreviewSrcRect.get();
-            final Rect dest = mCachedAppWidgetPreviewDestRect.get();
-            c.setBitmap(preview);
-            src.set(0, 0, defaultPreview.getWidth(), defaultPreview.getHeight());
-            dest.set(x, 0, x + previewWidth, previewHeight);
+            final Paint p = new Paint();
+            p.setFilterBitmap(true);
+            int appIconSize = LauncherAppState.getInstance().getDynamicGrid()
+                    .getDeviceProfile().iconSizePx;
 
-            Paint p = mCachedAppWidgetPreviewPaint.get();
-            if (p == null) {
-                p = new Paint();
-                p.setFilterBitmap(true);
-                mCachedAppWidgetPreviewPaint.set(p);
+            // draw the spanX x spanY tiles
+            final Rect src = new Rect(0, 0, tileBitmap.getWidth(), tileBitmap.getHeight());
+
+            float tileW = scale * tileBitmap.getWidth();
+            float tileH = scale * tileBitmap.getHeight();
+            final RectF dst = new RectF(0, 0, tileW, tileH);
+
+            float tx = x;
+            for (int i = 0; i < spanX; i++, tx += tileW) {
+                float ty = 0;
+                for (int j = 0; j < spanY; j++, ty += tileH) {
+                    dst.offsetTo(tx, ty);
+                    c.drawBitmap(tileBitmap, src, dst, p);
+                }
             }
-            c.drawBitmap(defaultPreview, src, dest, p);
+
+            // Draw the icon in the top left corner
+            // TODO: use top right for RTL
+            int minOffset = (int) (appIconSize * WIDGET_PREVIEW_ICON_PADDING_PERCENTAGE);
+            int smallestSide = Math.min(previewWidth, previewHeight);
+            float iconScale = Math.min((float) smallestSide / (appIconSize + 2 * minOffset), scale);
+
+            try {
+                Drawable icon = mutateOnMainThread(mManager.loadIcon(info, mIconCache));
+                if (icon != null) {
+                    int hoffset = (int) ((tileW - appIconSize * iconScale) / 2) + x;
+                    int yoffset = (int) ((tileH - appIconSize * iconScale) / 2);
+                    icon.setBounds(hoffset, yoffset,
+                            hoffset + (int) (appIconSize * iconScale),
+                            yoffset + (int) (appIconSize * iconScale));
+                    icon.draw(c);
+                }
+            } catch (Resources.NotFoundException e) { }
             c.setBitmap(null);
         }
         return mManager.getBadgeBitmap(info, preview);
@@ -634,71 +463,49 @@
 
     private Bitmap generateShortcutPreview(
             ResolveInfo info, int maxWidth, int maxHeight, Bitmap preview) {
-        Bitmap tempBitmap = mCachedShortcutPreviewBitmap.get();
-        final Canvas c = mCachedShortcutPreviewCanvas.get();
-        if (tempBitmap == null ||
-                tempBitmap.getWidth() != maxWidth ||
-                tempBitmap.getHeight() != maxHeight) {
-            tempBitmap = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888);
-            mCachedShortcutPreviewBitmap.set(tempBitmap);
-        } else {
-            c.setBitmap(tempBitmap);
-            c.drawColor(0, PorterDuff.Mode.CLEAR);
-            c.setBitmap(null);
-        }
-        // Render the icon
-        Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo));
-
-        int paddingTop = mContext.
-                getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top);
-        int paddingLeft = mContext.
-                getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left);
-        int paddingRight = mContext.
-                getResources().getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right);
-
-        int scaledIconWidth = (maxWidth - paddingLeft - paddingRight);
-
-        renderDrawableToBitmap(
-                icon, tempBitmap, paddingLeft, paddingTop, scaledIconWidth, scaledIconWidth);
-
-        if (preview != null &&
-                (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight)) {
-            throw new RuntimeException("Improperly sized bitmap passed as argument");
-        } else if (preview == null) {
+        final Canvas c = new Canvas();
+        if (preview == null) {
             preview = Bitmap.createBitmap(maxWidth, maxHeight, Config.ARGB_8888);
+            c.setBitmap(preview);
+        } else if (preview.getWidth() != maxWidth || preview.getHeight() != maxHeight) {
+            throw new RuntimeException("Improperly sized bitmap passed as argument");
+        } else {
+            // Reusing bitmap. Clear it.
+            c.setBitmap(preview);
+            c.drawColor(0, PorterDuff.Mode.CLEAR);
         }
 
-        c.setBitmap(preview);
+        Drawable icon = mutateOnMainThread(mIconCache.getFullResIcon(info.activityInfo));
+        icon.setFilterBitmap(true);
+
         // Draw a desaturated/scaled version of the icon in the background as a watermark
-        Paint p = mCachedShortcutPreviewPaint.get();
-        if (p == null) {
-            p = new Paint();
-            ColorMatrix colorMatrix = new ColorMatrix();
-            colorMatrix.setSaturation(0);
-            p.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
-            p.setAlpha((int) (255 * 0.06f));
-            mCachedShortcutPreviewPaint.set(p);
-        }
-        c.drawBitmap(tempBitmap, 0, 0, p);
+        ColorMatrix colorMatrix = new ColorMatrix();
+        colorMatrix.setSaturation(0);
+        icon.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
+        icon.setAlpha((int) (255 * 0.06f));
+
+        Resources res = mContext.getResources();
+        int paddingTop = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_top);
+        int paddingLeft = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_left);
+        int paddingRight = res.getDimensionPixelOffset(R.dimen.shortcut_preview_padding_right);
+        int scaledIconWidth = (maxWidth - paddingLeft - paddingRight);
+        icon.setBounds(paddingLeft, paddingTop,
+                paddingLeft + scaledIconWidth, paddingTop + scaledIconWidth);
+        icon.draw(c);
+
+        // Draw the final icon at top left corner.
+        // TODO: use top right for RTL
+        int appIconSize = LauncherAppState.getInstance().getDynamicGrid()
+                .getDeviceProfile().iconSizePx;
+        icon.setAlpha(255);
+        icon.setColorFilter(null);
+        icon.setBounds(0, 0, appIconSize, appIconSize);
+        icon.draw(c);
+
         c.setBitmap(null);
-
-        renderDrawableToBitmap(icon, preview, 0, 0, mAppIconSize, mAppIconSize);
-
         return preview;
     }
 
-    private static void renderDrawableToBitmap(
-            Drawable d, Bitmap bitmap, int x, int y, int w, int h) {
-        if (bitmap != null) {
-            Canvas c = new Canvas(bitmap);
-            Rect oldBounds = d.copyBounds();
-            d.setBounds(x, y, x + w, y + h);
-            d.draw(c);
-            d.setBounds(oldBounds); // Restore the bounds
-            c.setBitmap(null);
-        }
-    }
-
     private Drawable mutateOnMainThread(final Drawable drawable) {
         try {
             return mMainThreadExecutor.submit(new Callable<Drawable>() {
@@ -715,82 +522,148 @@
         }
     }
 
-    private static final int MAX_OPEN_FILES = 1024;
-    private static final int SAMPLE_RATE = 23;
     /**
-     * Dumps all files that are open in this process without allocating a file descriptor.
+     * @return an array of containing versionCode and lastUpdatedTime for the package.
      */
-    private static void dumpOpenFiles() {
-        try {
-            Log.i(TAG, "DUMP OF OPEN FILES (sample rate: 1 every " + SAMPLE_RATE + "):");
-            final String TYPE_APK = "apk";
-            final String TYPE_JAR = "jar";
-            final String TYPE_PIPE = "pipe";
-            final String TYPE_SOCKET = "socket";
-            final String TYPE_DB = "db";
-            final String TYPE_ANON_INODE = "anon_inode";
-            final String TYPE_DEV = "dev";
-            final String TYPE_NON_FS = "non-fs";
-            final String TYPE_OTHER = "other";
-            List<String> types = Arrays.asList(TYPE_APK, TYPE_JAR, TYPE_PIPE, TYPE_SOCKET, TYPE_DB,
-                    TYPE_ANON_INODE, TYPE_DEV, TYPE_NON_FS, TYPE_OTHER);
-            int[] count = new int[types.size()];
-            int[] duplicates = new int[types.size()];
-            HashSet<String> files = new HashSet<String>();
-            int total = 0;
-            for (int i = 0; i < MAX_OPEN_FILES; i++) {
-                // This is a gigantic hack but unfortunately the only way to resolve an fd
-                // to a file name. Note that we have to loop over all possible fds because
-                // reading the directory would require allocating a new fd. The kernel is
-                // currently implemented such that no fd is larger then the current rlimit,
-                // which is why it's safe to loop over them in such a way.
-                String fd = "/proc/self/fd/" + i;
+    private long[] getPackageVersion(String packageName) {
+        synchronized (mPackageVersions) {
+            long[] versions = mPackageVersions.get(packageName);
+            if (versions == null) {
+                versions = new long[2];
                 try {
-                    // getCanonicalPath() uses readlink behind the scene which doesn't require
-                    // a file descriptor.
-                    String resolved = new File(fd).getCanonicalPath();
-                    int type = types.indexOf(TYPE_OTHER);
-                    if (resolved.startsWith("/dev/")) {
-                        type = types.indexOf(TYPE_DEV);
-                    } else if (resolved.endsWith(".apk")) {
-                        type = types.indexOf(TYPE_APK);
-                    } else if (resolved.endsWith(".jar")) {
-                        type = types.indexOf(TYPE_JAR);
-                    } else if (resolved.contains("/fd/pipe:")) {
-                        type = types.indexOf(TYPE_PIPE);
-                    } else if (resolved.contains("/fd/socket:")) {
-                        type = types.indexOf(TYPE_SOCKET);
-                    } else if (resolved.contains("/fd/anon_inode:")) {
-                        type = types.indexOf(TYPE_ANON_INODE);
-                    } else if (resolved.endsWith(".db") || resolved.contains("/databases/")) {
-                        type = types.indexOf(TYPE_DB);
-                    } else if (resolved.startsWith("/proc/") && resolved.contains("/fd/")) {
-                        // Those are the files that don't point anywhere on the file system.
-                        // getCanonicalPath() wrongly interprets these as relative symlinks and
-                        // resolves them within /proc/<pid>/fd/.
-                        type = types.indexOf(TYPE_NON_FS);
+                    PackageInfo info = mContext.getPackageManager().getPackageInfo(packageName, 0);
+                    versions[0] = info.versionCode;
+                    versions[1] = info.lastUpdateTime;
+                } catch (NameNotFoundException e) {
+                    Log.e(TAG, "PackageInfo not found", e);
+                }
+                mPackageVersions.put(packageName, versions);
+            }
+            return versions;
+        }
+    }
+
+    /**
+     * A request Id which can be used by the client to cancel any request.
+     */
+    public class PreviewLoadRequest {
+
+        private final PreviewLoadTask mTask;
+        private final WidgetCacheKey mKey;
+
+        public PreviewLoadRequest(PreviewLoadTask task, WidgetCacheKey key) {
+            mTask = task;
+            mKey = key;
+        }
+
+        public void cancel(boolean recycleImage) {
+            if (mTask != null) {
+                mTask.cancel(true);
+            }
+
+            if (recycleImage) {
+                synchronized(mLoadedPreviews) {
+                    WeakReference<Bitmap> result = mLoadedPreviews.remove(mKey);
+                    if (result != null && result.get() != null) {
+                        mUnusedBitmaps.add(result.get());
                     }
-                    count[type]++;
-                    total++;
-                    if (files.contains(resolved)) {
-                        duplicates[type]++;
-                    }
-                    files.add(resolved);
-                    if (total % SAMPLE_RATE == 0) {
-                        Log.i(TAG, " fd " + i + ": " + resolved
-                                + " (" + types.get(type) + ")");
-                    }
-                } catch (IOException e) {
-                    // Ignoring exceptions for non-existing file descriptors.
                 }
             }
-            for (int i = 0; i < types.size(); i++) {
-                Log.i(TAG, String.format("Open %10s files: %4d total, %4d duplicates",
-                        types.get(i), count[i], duplicates[i]));
+        }
+    }
+
+    public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> {
+
+        private final WidgetCacheKey mKey;
+        private final Object mInfo;
+        private final int mPreviewHeight;
+        private final int mPreviewWidth;
+        private final WidgetCell mCaller;
+
+        PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth,
+                int previewHeight, WidgetCell caller) {
+            mKey = key;
+            mInfo = info;
+            mPreviewHeight = previewHeight;
+            mPreviewWidth = previewWidth;
+            mCaller = caller;
+            if (DEBUG) {
+                Log.d(TAG, String.format("%s, %s, %d, %d",
+                        mKey, mInfo, mPreviewHeight, mPreviewWidth));
             }
-        } catch (Throwable t) {
-            // Catch everything. This is called from an exception handler that we shouldn't upset.
-            Log.e(TAG, "Unable to log open files.", t);
+        }
+
+        @Override
+        protected Bitmap doInBackground(Void... params) {
+            Bitmap unusedBitmap = null;
+
+            // TODO(hyunyoungs): Figure out why this path causes concurrency issue.
+            synchronized (mUnusedBitmaps) {
+                // Check if we can use a bitmap
+                for (Bitmap candidate : mUnusedBitmaps) {
+                    if (candidate != null && candidate.isMutable() &&
+                            candidate.getWidth() == mPreviewWidth &&
+                            candidate.getHeight() == mPreviewHeight) {
+                        unusedBitmap = candidate;
+                        break;
+                    }
+                }
+
+                if (unusedBitmap == null) {
+                    unusedBitmap = Bitmap.createBitmap(mPreviewWidth, mPreviewHeight, Config.ARGB_8888);
+                } else {
+                    mUnusedBitmaps.remove(unusedBitmap);
+                }
+            }
+            if (isCancelled()) {
+                return null;
+            }
+            Bitmap preview = readFromDb(mKey, unusedBitmap);
+            if (!isCancelled() && preview == null) {
+                // Fetch the version info before we generate the preview, so that, in-case the
+                // app was updated while we are generating the preview, we use the old version info,
+                // which would gets re-written next time.
+                long[] versions = getPackageVersion(mKey.componentName.getPackageName());
+
+                // it's not in the db... we need to generate it
+                preview = generatePreview(mInfo, unusedBitmap, mPreviewWidth, mPreviewHeight);
+
+                if (!isCancelled()) {
+                    writeToDb(mKey, versions, preview);
+                }
+            }
+
+            return preview;
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap result) {
+            synchronized(mLoadedPreviews) {
+                mLoadedPreviews.put(mKey, new WeakReference<Bitmap>(result));
+            }
+
+            mCaller.applyPreview(result);
+        }
+    }
+
+    private static final class WidgetCacheKey extends ComponentKey {
+
+        // TODO: remove dependency on size
+        private final String size;
+
+        public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) {
+            super(componentName, user);
+            this.size = size;
+        }
+
+        @Override
+        public int hashCode() {
+            return super.hashCode() ^ size.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return super.equals(o) && ((WidgetCacheKey) o).size.equals(size);
         }
     }
 }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 9521540..6b03e31 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -26,6 +26,7 @@
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.TargetApi;
 import android.app.WallpaperManager;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetProviderInfo;
@@ -43,12 +44,11 @@
 import android.graphics.Rect;
 import android.graphics.Region.Op;
 import android.graphics.drawable.Drawable;
-import android.net.Uri;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Parcelable;
-import android.support.v4.view.ViewCompat;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.SparseArray;
@@ -66,9 +66,14 @@
 import com.android.launcher3.Launcher.CustomContentCallbacks;
 import com.android.launcher3.Launcher.LauncherOverlay;
 import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.UninstallDropTarget.UninstallSource;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo;
 import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.WallpaperUtils;
+import com.android.launcher3.widget.PendingAddShortcutInfo;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -84,7 +89,7 @@
 public class Workspace extends SmoothPagedView
         implements DropTarget, DragSource, DragScroller, View.OnTouchListener,
         DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener,
-        Insettable {
+        Insettable, UninstallSource {
     private static final String TAG = "Launcher.Workspace";
 
     private static final int CHILDREN_OUTLINE_FADE_OUT_DELAY = 0;
@@ -117,24 +122,24 @@
     private long mCustomContentShowTime = -1;
 
     private LayoutTransition mLayoutTransition;
-    private final WallpaperManager mWallpaperManager;
-    private IBinder mWindowToken;
+    @Thunk final WallpaperManager mWallpaperManager;
+    @Thunk IBinder mWindowToken;
 
     private int mOriginalDefaultPage;
     private int mDefaultPage;
 
     private ShortcutAndWidgetContainer mDragSourceInternal;
-    private static boolean sAccessibilityEnabled;
+    @Thunk static boolean sAccessibilityEnabled;
 
     // The screen id used for the empty screen always present to the right.
     final static long EXTRA_EMPTY_SCREEN_ID = -201;
     private final static long CUSTOM_CONTENT_SCREEN_ID = -301;
 
-    private HashMap<Long, CellLayout> mWorkspaceScreens = new HashMap<Long, CellLayout>();
-    private ArrayList<Long> mScreenOrder = new ArrayList<Long>();
+    @Thunk HashMap<Long, CellLayout> mWorkspaceScreens = new HashMap<Long, CellLayout>();
+    @Thunk ArrayList<Long> mScreenOrder = new ArrayList<Long>();
 
-    private Runnable mRemoveEmptyScreenRunnable;
-    private boolean mDeferRemoveExtraEmptyScreen = false;
+    @Thunk Runnable mRemoveEmptyScreenRunnable;
+    @Thunk boolean mDeferRemoveExtraEmptyScreen = false;
 
     /**
      * CellInfo for the cell that is currently being dragged
@@ -144,7 +149,7 @@
     /**
      * Target drop area calculated during last acceptDrop call.
      */
-    private int[] mTargetCell = new int[2];
+    @Thunk int[] mTargetCell = new int[2];
     private int mDragOverX = -1;
     private int mDragOverY = -1;
 
@@ -159,7 +164,7 @@
     /**
      * The CellLayout that is currently being dragged over
      */
-    private CellLayout mDragTargetLayout = null;
+    @Thunk CellLayout mDragTargetLayout = null;
     /**
      * The CellLayout that we will show as glowing
      */
@@ -170,16 +175,16 @@
      */
     private CellLayout mDropToLayout = null;
 
-    private Launcher mLauncher;
-    private IconCache mIconCache;
-    private DragController mDragController;
+    @Thunk Launcher mLauncher;
+    @Thunk IconCache mIconCache;
+    @Thunk DragController mDragController;
 
     // These are temporary variables to prevent having to allocate a new object just to
     // return an (x, y) value from helper functions. Do NOT use them to maintain other state.
     private int[] mTempCell = new int[2];
     private int[] mTempPt = new int[2];
     private int[] mTempEstimate = new int[2];
-    private float[] mDragViewVisualCenter = new float[2];
+    @Thunk float[] mDragViewVisualCenter = new float[2];
     private float[] mTempCellLayoutCenterCoordinates = new float[2];
     private Matrix mTempInverseMatrix = new Matrix();
 
@@ -204,7 +209,7 @@
     private boolean mInScrollArea = false;
 
     private HolographicOutlineHelper mOutlineHelper;
-    private Bitmap mDragOutline = null;
+    @Thunk Bitmap mDragOutline = null;
     private static final Rect sTempRect = new Rect();
     private final int[] mTempXY = new int[2];
     private int[] mTempVisiblePagesRange = new int[2];
@@ -213,11 +218,11 @@
     private boolean mWorkspaceFadeInAdjacentScreens;
 
     WallpaperOffsetInterpolator mWallpaperOffset;
-    private boolean mWallpaperIsLiveWallpaper;
-    private int mNumPagesForWallpaperParallax;
-    private float mLastSetWallpaperOffsetSteps = 0;
+    @Thunk boolean mWallpaperIsLiveWallpaper;
+    @Thunk int mNumPagesForWallpaperParallax;
+    @Thunk float mLastSetWallpaperOffsetSteps = 0;
 
-    private Runnable mDelayedResizeRunnable;
+    @Thunk Runnable mDelayedResizeRunnable;
     private Runnable mDelayedSnapToPageRunnable;
     private Point mDisplaySize = new Point();
 
@@ -226,7 +231,7 @@
     public static final int REORDER_TIMEOUT = 350;
     private final Alarm mFolderCreationAlarm = new Alarm();
     private final Alarm mReorderAlarm = new Alarm();
-    private FolderRingAnimator mDragFolderRingAnimator = null;
+    @Thunk FolderRingAnimator mDragFolderRingAnimator = null;
     private FolderIcon mDragOverFolderIcon = null;
     private boolean mCreateUserFolderOnDrop = false;
     private boolean mAddToExistingFolderOnDrop = false;
@@ -255,8 +260,8 @@
     private static final int DRAG_MODE_ADD_TO_FOLDER = 2;
     private static final int DRAG_MODE_REORDER = 3;
     private int mDragMode = DRAG_MODE_NONE;
-    private int mLastReorderX = -1;
-    private int mLastReorderY = -1;
+    @Thunk int mLastReorderX = -1;
+    @Thunk int mLastReorderY = -1;
 
     private SparseArray<Parcelable> mSavedStates;
     private final ArrayList<Integer> mRestoredPages = new ArrayList<Integer>();
@@ -268,17 +273,17 @@
 
     private float mCurrentScale;
     private float mNewScale;
-    private float[] mOldBackgroundAlphas;
+    @Thunk float[] mOldBackgroundAlphas;
     private float[] mOldAlphas;
-    private float[] mNewBackgroundAlphas;
+    @Thunk float[] mNewBackgroundAlphas;
     private float[] mNewAlphas;
     private int mLastChildCount = -1;
     private float mTransitionProgress;
-    private Animator mStateAnimator = null;
+    @Thunk Animator mStateAnimator = null;
 
     float mOverScrollEffect = 0f;
 
-    private Runnable mDeferredAction;
+    @Thunk Runnable mDeferredAction;
     private boolean mDeferDropAfterUninstall;
     private boolean mUninstallSuccessful;
 
@@ -365,13 +370,12 @@
 
     // estimate the size of a widget with spans hSpan, vSpan. return MAX_VALUE for each
     // dimension if unsuccessful
-    public int[] estimateItemSize(int hSpan, int vSpan,
-            ItemInfo itemInfo, boolean springLoaded) {
+    public int[] estimateItemSize(ItemInfo itemInfo, boolean springLoaded) {
         int[] size = new int[2];
         if (getChildCount() > 0) {
             // Use the first non-custom page to estimate the child position
             CellLayout cl = (CellLayout) getChildAt(numCustomPages());
-            Rect r = estimateItemPosition(cl, itemInfo, 0, 0, hSpan, vSpan);
+            Rect r = estimateItemPosition(cl, itemInfo, 0, 0, itemInfo.spanX, itemInfo.spanY);
             size[0] = r.width();
             size[1] = r.height();
             if (springLoaded) {
@@ -401,7 +405,6 @@
         setChildrenBackgroundAlphaMultipliers(1f);
         // Prevent any Un/InstallShortcutReceivers from updating the db while we are dragging
         InstallShortcutReceiver.enableInstallQueue();
-        UninstallShortcutReceiver.enableUninstallQueue();
         post(new Runnable() {
             @Override
             public void run() {
@@ -429,7 +432,6 @@
 
         // Re-enable any Un/InstallShortcutReceiver and now process any queued items
         InstallShortcutReceiver.disableAndFlushInstallQueue(getContext());
-        UninstallShortcutReceiver.disableAndFlushUninstallQueue(getContext());
 
         mDragSourceInternal = null;
         mLauncher.onInteractionEnd();
@@ -492,7 +494,7 @@
         CellLayout cl = ((CellLayout) child);
         cl.setOnInterceptTouchListener(this);
         cl.setClickable(true);
-        cl.setImportantForAccessibility(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
+        cl.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
         super.onChildViewAdded(parent, child);
     }
 
@@ -561,10 +563,6 @@
     }
 
     public long insertNewWorkspaceScreen(long screenId, int insertIndex) {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - insertNewWorkspaceScreen(): " + screenId +
-                " at index: " + insertIndex, true);
-
         if (mWorkspaceScreens.containsKey(screenId)) {
             throw new RuntimeException("Screen id " + screenId + " already exists!");
         }
@@ -578,6 +576,12 @@
         mWorkspaceScreens.put(screenId, newScreen);
         mScreenOrder.add(insertIndex, screenId);
         addView(newScreen, insertIndex);
+
+        LauncherAccessibilityDelegate delegate =
+                LauncherAppState.getInstance().getAccessibilityDelegate();
+        if (delegate != null && delegate.isInAccessibleDrag()) {
+            newScreen.enableAccessibleDrag(true);
+        }
         return screenId;
     }
 
@@ -664,9 +668,6 @@
     }
 
     public void addExtraEmptyScreenOnDrag() {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreenOnDrag()", true);
-
         boolean lastChildOnScreen = false;
         boolean childOnFinalScreen = false;
 
@@ -693,9 +694,6 @@
     }
 
     public boolean addExtraEmptyScreen() {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - addExtraEmptyScreen()", true);
-
         if (!mWorkspaceScreens.containsKey(EXTRA_EMPTY_SCREEN_ID)) {
             insertNewWorkspaceScreen(EXTRA_EMPTY_SCREEN_ID);
             return true;
@@ -704,9 +702,6 @@
     }
 
     private void convertFinalScreenToEmptyScreenIfNecessary() {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - convertFinalScreenToEmptyScreenIfNecessary()", true);
-
         if (mLauncher.isWorkspaceLoading()) {
             // Invalid and dangerous operation if workspace is loading
             Launcher.addDumpLog(TAG, "    - workspace loading, skip", true);
@@ -731,7 +726,6 @@
 
             // Update the model if we have changed any screens
             mLauncher.getModel().updateWorkspaceScreenOrder(mLauncher, mScreenOrder);
-            Launcher.addDumpLog(TAG, "11683562 -   extra empty screen: " + finalScreenId, true);
         }
     }
 
@@ -741,8 +735,6 @@
 
     public void removeExtraEmptyScreenDelayed(final boolean animate, final Runnable onComplete,
             final int delay, final boolean stripEmptyScreens) {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - removeExtraEmptyScreen()", true);
         if (mLauncher.isWorkspaceLoading()) {
             // Don't strip empty screens if the workspace is still loading
             Launcher.addDumpLog(TAG, "    - workspace loading, skip", true);
@@ -784,9 +776,7 @@
 
     private void fadeAndRemoveEmptyScreen(int delay, int duration, final Runnable onComplete,
             final boolean stripEmptyScreens) {
-        // Log to disk
         // XXX: Do we need to update LM workspace screens below?
-        Launcher.addDumpLog(TAG, "11683562 - fadeAndRemoveEmptyScreen()", true);
         PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 0f);
         PropertyValuesHolder bgAlpha = PropertyValuesHolder.ofFloat("backgroundAlpha", 0f);
 
@@ -830,8 +820,6 @@
     }
 
     public long commitExtraEmptyScreen() {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - commitExtraEmptyScreen()", true);
         if (mLauncher.isWorkspaceLoading()) {
             // Invalid and dangerous operation if workspace is loading
             Launcher.addDumpLog(TAG, "    - workspace loading, skip", true);
@@ -890,9 +878,6 @@
     }
 
     public void stripEmptyScreens() {
-        // Log to disk
-        Launcher.addDumpLog(TAG, "11683562 - stripEmptyScreens()", true);
-
         if (mLauncher.isWorkspaceLoading()) {
             // Don't strip empty screens if the workspace is still loading.
             // This is dangerous and can result in data loss.
@@ -920,7 +905,6 @@
 
         int pageShift = 0;
         for (Long id: removeScreens) {
-            Launcher.addDumpLog(TAG, "11683562 -   removing id: " + id, true);
             CellLayout cl = mWorkspaceScreens.get(id);
             mWorkspaceScreens.remove(id);
             mScreenOrder.remove(id);
@@ -1334,10 +1318,10 @@
     protected void setWallpaperDimension() {
         new AsyncTask<Void, Void, Void>() {
             public Void doInBackground(Void ... args) {
-                String spKey = WallpaperCropActivity.getSharedPreferencesKey();
+                String spKey = LauncherFiles.WALLPAPER_CROP_PREFERENCES_KEY;
                 SharedPreferences sp =
                         mLauncher.getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS);
-                LauncherWallpaperPickerActivity.suggestWallpaperDimension(mLauncher.getResources(),
+                WallpaperUtils.suggestWallpaperDimension(mLauncher.getResources(),
                         sp, mLauncher.getWindowManager(), mWallpaperManager,
                         mLauncher.overrideWallpaperDimensions());
                 return null;
@@ -1427,7 +1411,22 @@
         }
 
         private float wallpaperOffsetForCurrentScroll() {
+            // TODO: do different behavior if it's  a live wallpaper?
+            // Don't use up all the wallpaper parallax until you have at least
+            // MIN_PARALLAX_PAGE_SPAN pages
+            int numScrollingPages = getNumScreensExcludingEmptyAndCustom();
+            int parallaxPageSpan;
+            if (mWallpaperIsLiveWallpaper) {
+                parallaxPageSpan = numScrollingPages - 1;
+            } else {
+                parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1);
+            }
+            mNumPagesForWallpaperParallax = parallaxPageSpan;
+
             if (getChildCount() <= 1) {
+                if (isLayoutRtl()) {
+                    return 1 - 1.0f/mNumPagesForWallpaperParallax;
+                }
                 return 0;
             }
 
@@ -1447,28 +1446,20 @@
             if (scrollRange == 0) {
                 return 0;
             } else {
-                // TODO: do different behavior if it's  a live wallpaper?
                 // Sometimes the left parameter of the pages is animated during a layout transition;
                 // this parameter offsets it to keep the wallpaper from animating as well
                 int adjustedScroll =
                         getScrollX() - firstPageScrollX - getLayoutTransitionOffsetForPage(0);
                 float offset = Math.min(1, adjustedScroll / (float) scrollRange);
                 offset = Math.max(0, offset);
-                // Don't use up all the wallpaper parallax until you have at least
-                // MIN_PARALLAX_PAGE_SPAN pages
-                int numScrollingPages = getNumScreensExcludingEmptyAndCustom();
-                int parallaxPageSpan;
-                if (mWallpaperIsLiveWallpaper) {
-                    parallaxPageSpan = numScrollingPages - 1;
-                } else {
-                    parallaxPageSpan = Math.max(MIN_PARALLAX_PAGE_SPAN, numScrollingPages - 1);
-                }
-                mNumPagesForWallpaperParallax = parallaxPageSpan;
 
                 // On RTL devices, push the wallpaper offset to the right if we don't have enough
                 // pages (ie if numScrollingPages < MIN_PARALLAX_PAGE_SPAN)
-                int padding = isLayoutRtl() ? parallaxPageSpan - numScrollingPages + 1 : 0;
-                return offset * (padding + numScrollingPages - 1) / parallaxPageSpan;
+                if (!mWallpaperIsLiveWallpaper && numScrollingPages < MIN_PARALLAX_PAGE_SPAN
+                        && isLayoutRtl()) {
+                    return offset * (parallaxPageSpan - numScrollingPages + 1) / parallaxPageSpan;
+                }
+                return offset * (numScrollingPages - 1) / parallaxPageSpan;
             }
         }
 
@@ -1548,7 +1539,7 @@
     @Override
     public void announceForAccessibility(CharSequence text) {
         // Don't announce if apps is on top of us.
-        if (!mLauncher.isAllAppsVisible()) {
+        if (!mLauncher.isAppsViewVisible()) {
             super.announceForAccessibility(text);
         }
     }
@@ -1647,7 +1638,6 @@
                     float scrollProgress = getScrollProgress(screenCenter, child, i);
                     float alpha = 1 - Math.abs(scrollProgress);
                     child.getShortcutsAndWidgets().setAlpha(alpha);
-                    //child.setBackgroundAlphaMultiplier(1 - alpha);
                 }
             }
         }
@@ -1660,6 +1650,23 @@
         }
     }
 
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public void enableAccessibleDrag(boolean enable) {
+        for (int i = 0; i < getChildCount(); i++) {
+            CellLayout child = (CellLayout) getChildAt(i);
+            child.enableAccessibleDrag(enable);
+        }
+
+        if (enable) {
+            // We need to allow our individual children to become click handlers in this case
+            setOnClickListener(null);
+        } else {
+            // Reset our click listener
+            setOnClickListener(mLauncher);
+        }
+        mLauncher.getHotseat().getLayout().enableAccessibleDrag(enable);
+    }
+
     public boolean hasCustomContent() {
         return (mScreenOrder.size() > 0 && mScreenOrder.get(0) == CUSTOM_CONTENT_SCREEN_ID);
     }
@@ -1701,7 +1708,11 @@
 
         mLastCustomContentScrollProgress = progress;
 
-        mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f);
+        // We should only update the drag layer background alpha if we are not in all apps or the
+        // widgets tray
+        if (mState == State.NORMAL) {
+            mLauncher.getDragLayer().setBackgroundAlpha(progress * 0.8f);
+        }
 
         if (mLauncher.getHotseat() != null) {
             mLauncher.getHotseat().setTranslationX(translationX);
@@ -1820,7 +1831,7 @@
 
     @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
-        if (!mLauncher.isAllAppsVisible()) {
+        if (!mLauncher.isAppsViewVisible()) {
             final Folder openFolder = getOpenFolder();
             if (openFolder != null) {
                 return openFolder.requestFocus(direction, previouslyFocusedRect);
@@ -1841,7 +1852,7 @@
 
     @Override
     public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
-        if (!mLauncher.isAllAppsVisible()) {
+        if (!mLauncher.isAppsViewVisible()) {
             final Folder openFolder = getOpenFolder();
             if (openFolder != null) {
                 openFolder.addFocusables(views, direction);
@@ -1886,7 +1897,7 @@
         }
     }
 
-    private void updateChildrenLayersEnabled(boolean force) {
+    @Thunk void updateChildrenLayersEnabled(boolean force) {
         boolean small = mState == State.OVERVIEW || mIsSwitchingState;
         boolean enableChildrenLayers = force || small || mAnimatingViewIntoPlace || isPageMoving();
 
@@ -2050,8 +2061,7 @@
 
         // If this is a text view, use its drawable instead
         if (v instanceof TextView) {
-            TextView tv = (TextView) v;
-            Drawable d = tv.getCompoundDrawables()[1];
+            Drawable d = getTextViewIcon((TextView) v);
             Rect bounds = getDrawableBounds(d);
             bmpWidth = bounds.width();
             bmpHeight = bounds.height();
@@ -2069,7 +2079,7 @@
     }
 
     public void onDragStartedWithItem(PendingAddItemInfo info, Bitmap b, boolean clipAlpha) {
-        int[] size = estimateItemSize(info.spanX, info.spanY, info, false);
+        int[] size = estimateItemSize(info, false);
 
         // The outline is used to visualize where the item will land if dropped
         mDragOutline = createDragOutline(b, DRAG_BITMAP_PADDING, size[0], size[1], clipAlpha);
@@ -2210,8 +2220,8 @@
 
     private void updateAccessibilityFlags() {
         int accessible = mState == State.NORMAL ?
-                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES :
-                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
+                IMPORTANT_FOR_ACCESSIBILITY_NO :
+                IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS;
         setImportantForAccessibility(accessible);
     }
 
@@ -2254,7 +2264,10 @@
         float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f;
         float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f;
         float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f;
-        float finalSearchBarAlpha = !stateIsNormal ? 0f : 1f;
+        // We keep the search bar visible on the workspace and in AllApps now
+        boolean showSearchBar = stateIsNormal ||
+                (mLauncher.isAllAppsSearchOverridden() && stateIsNormalHidden);
+        float finalSearchBarAlpha = showSearchBar ? 1f : 0f;
         float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ?
                 getOverviewModeTranslationY() : 0;
 
@@ -2331,7 +2344,7 @@
             }
         }
 
-        final View searchBar = mLauncher.getQsbBar();
+        final View searchBar = mLauncher.getOrCreateQsbBar();
         final View overviewPanel = mLauncher.getOverviewPanel();
         final View hotseat = mLauncher.getHotseat();
         final View pageIndicator = getPageIndicator();
@@ -2352,7 +2365,7 @@
                     cl.setShortcutAndWidgetAlpha(mNewAlphas[i]);
                 } else {
                     if (layerViews != null) {
-                        layerViews.put(cl, Launcher.BUILD_LAYER);
+                        layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER);
                     }
                     if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) {
                         LauncherViewPropertyAnimator alphaAnim =
@@ -2404,8 +2417,8 @@
             if (layerViews != null) {
                 // If layerViews is not null, we add these views, and indicate that
                 // the caller can manage layer state.
-                layerViews.put(hotseat, Launcher.BUILD_AND_SET_LAYER);
-                layerViews.put(overviewPanel, Launcher.BUILD_AND_SET_LAYER);
+                layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
+                layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
             } else {
                 // Otherwise let the animator handle layer management.
                 hotseatAlpha.withLayer();
@@ -2434,7 +2447,7 @@
                 if (layerViews != null) {
                     // If layerViews is not null, we add these views, and indicate that
                     // the caller can manage layer state.
-                    layerViews.put(searchBar, Launcher.BUILD_AND_SET_LAYER);
+                    layerViews.put(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
                 } else {
                     // Otherwise let the animator handle layer management.
                     searchBarAlpha.withLayer();
@@ -2576,7 +2589,7 @@
         }
     }
 
-    private void onTransitionEnd() {
+    @Thunk void onTransitionEnd() {
         mIsSwitchingState = false;
         updateChildrenLayersEnabled(false);
         showCustomContentIfNecessary();
@@ -2588,6 +2601,19 @@
     }
 
     /**
+     * Returns the drawable for the given text view.
+     */
+    public static Drawable getTextViewIcon(TextView tv) {
+        final Drawable[] drawables = tv.getCompoundDrawables();
+        for (int i = 0; i < drawables.length; i++) {
+            if (drawables[i] != null) {
+                return drawables[i];
+            }
+        }
+        return null;
+    }
+
+    /**
      * Draw the View v into the given Canvas.
      *
      * @param v the view to draw
@@ -2602,7 +2628,7 @@
 
         destCanvas.save();
         if (v instanceof TextView) {
-            Drawable d = ((TextView) v).getCompoundDrawables()[1];
+            Drawable d = getTextViewIcon((TextView) v);
             Rect bounds = getDrawableBounds(d);
             clipRect.set(0, 0, bounds.width() + padding, bounds.height() + padding);
             destCanvas.translate(padding / 2 - bounds.left, padding / 2 - bounds.top);
@@ -2639,7 +2665,7 @@
 
         int padding = expectedPadding.get();
         if (v instanceof TextView) {
-            Drawable d = ((TextView) v).getCompoundDrawables()[1];
+            Drawable d = getTextViewIcon((TextView) v);
             Rect bounds = getDrawableBounds(d);
             b = Bitmap.createBitmap(bounds.width() + padding,
                     bounds.height() + padding, Bitmap.Config.ARGB_8888);
@@ -2700,7 +2726,11 @@
         return b;
     }
 
-    void startDrag(CellLayout.CellInfo cellInfo) {
+    public void startDrag(CellLayout.CellInfo cellInfo) {
+        startDrag(cellInfo, false);
+    }
+
+    public void startDrag(CellLayout.CellInfo cellInfo, boolean accessible) {
         View child = cellInfo.cell;
 
         // Make sure the drag was started by a long press as opposed to a long click.
@@ -2713,10 +2743,15 @@
         CellLayout layout = (CellLayout) child.getParent().getParent();
         layout.prepareChildForDrag(child);
 
-        beginDragShared(child, this);
+        beginDragShared(child, this, accessible);
     }
 
-    public void beginDragShared(View child, DragSource source) {
+    public void beginDragShared(View child, DragSource source, boolean accessible) {
+        beginDragShared(child, new Point(), source, accessible);
+    }
+
+    public void beginDragShared(View child, Point relativeTouchPos, DragSource source,
+            boolean accessible) {
         child.clearFocus();
         child.setPressed(false);
 
@@ -2741,11 +2776,23 @@
         Point dragVisualizeOffset = null;
         Rect dragRect = null;
         if (child instanceof BubbleTextView) {
+            BubbleTextView icon = (BubbleTextView) child;
             int iconSize = grid.iconSizePx;
             int top = child.getPaddingTop();
             int left = (bmpWidth - iconSize) / 2;
             int right = left + iconSize;
             int bottom = top + iconSize;
+            if (icon.isLayoutHorizontal()) {
+                // If the layout is horizontal, then if we are just picking up the icon, then just
+                // use the child position since the icon is top-left aligned.  Otherwise, offset
+                // the drag layer position horizontally so that the icon is under the current
+                // touch position.
+                if (icon.getIcon().getBounds().contains(relativeTouchPos.x, relativeTouchPos.y)) {
+                    dragLayerX = Math.round(mTempXY[0]);
+                } else {
+                    dragLayerX = Math.round(mTempXY[0] + relativeTouchPos.x - (bmpWidth / 2));
+                }
+            }
             dragLayerY += top;
             // Note: The drag region is used to calculate drag layer offsets, but the
             // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
@@ -2770,7 +2817,7 @@
         }
 
         DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
-                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
+                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, accessible);
         dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
 
         if (child.getParent() instanceof ShortcutAndWidgetContainer) {
@@ -2820,25 +2867,13 @@
 
         // Start the drag
         DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),
-                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
+                DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale, false);
         dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
 
         // Recycle temporary bitmaps
         tmpB.recycle();
     }
 
-    void addApplicationShortcut(ShortcutInfo info, CellLayout target, long container, long screenId,
-            int cellX, int cellY, boolean insertAtFirst, int intersectX, int intersectY) {
-        View view = mLauncher.createShortcut(R.layout.application, target, (ShortcutInfo) info);
-
-        final int[] cellXY = new int[2];
-        target.findCellForSpanThatIntersects(cellXY, 1, 1, intersectX, intersectY);
-        addInScreen(view, container, screenId, cellXY[0], cellXY[1], 1, 1, insertAtFirst);
-
-        LauncherModel.addOrMoveItemInDatabase(mLauncher, info, container, screenId, cellXY[0],
-                cellXY[1]);
-    }
-
     public boolean transitionStateShouldAllowDrop() {
         return ((!isSwitchingState() || mTransitionProgress > 0.5f) &&
                 (mState == State.NORMAL || mState == State.SPRING_LOADED));
@@ -2857,8 +2892,7 @@
             }
             if (!transitionStateShouldAllowDrop()) return false;
 
-            mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
-                    d.dragView, mDragViewVisualCenter);
+            mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
 
             // We want the point to be mapped to the dragTarget.
             if (mLauncher.isHotseatLayout(dropTargetLayout)) {
@@ -3060,9 +3094,7 @@
     }
 
     public void onDrop(final DragObject d) {
-        mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset, d.dragView,
-                mDragViewVisualCenter);
-
+        mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
         CellLayout dropTargetLayout = mDropToLayout;
 
         // We want the point to be mapped to the dragTarget.
@@ -3178,7 +3210,8 @@
 
                         final LauncherAppWidgetHostView hostView = (LauncherAppWidgetHostView) cell;
                         AppWidgetProviderInfo pInfo = hostView.getAppWidgetInfo();
-                        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE) {
+                        if (pInfo != null && pInfo.resizeMode != AppWidgetProviderInfo.RESIZE_NONE
+                                && !d.accessibleDrag) {
                             final Runnable addResizeFrame = new Runnable() {
                                 public void run() {
                                     DragLayer dragLayer = mLauncher.getDragLayer();
@@ -3568,38 +3601,6 @@
         return bestMatchingScreen;
     }
 
-    // This is used to compute the visual center of the dragView. This point is then
-    // used to visualize drop locations and determine where to drop an item. The idea is that
-    // the visual center represents the user's interpretation of where the item is, and hence
-    // is the appropriate point to use when determining drop location.
-    private float[] getDragViewVisualCenter(int x, int y, int xOffset, int yOffset,
-            DragView dragView, float[] recycle) {
-        float res[];
-        if (recycle == null) {
-            res = new float[2];
-        } else {
-            res = recycle;
-        }
-
-        // First off, the drag view has been shifted in a way that is not represented in the
-        // x and y values or the x/yOffsets. Here we account for that shift.
-        x += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetX);
-        y += getResources().getDimensionPixelSize(R.dimen.dragViewOffsetY);
-
-        // These represent the visual top and left of drag view if a dragRect was provided.
-        // If a dragRect was not provided, then they correspond to the actual view left and
-        // top, as the dragRect is in that case taken to be the entire dragView.
-        // R.dimen.dragViewOffsetY.
-        int left = x - xOffset;
-        int top = y - yOffset;
-
-        // In order to find the visual center, we shift by half the dragRect
-        res[0] = left + dragView.getDragRegion().width() / 2;
-        res[1] = top + dragView.getDragRegion().height() / 2;
-
-        return res;
-    }
-
     private boolean isDragWidget(DragObject d) {
         return (d.dragInfo instanceof LauncherAppWidgetInfo ||
                 d.dragInfo instanceof PendingAddWidgetInfo);
@@ -3624,8 +3625,7 @@
 
         // Ensure that we have proper spans for the item that we are dropping
         if (item.spanX < 0 || item.spanY < 0) throw new RuntimeException("Improper spans found");
-        mDragViewVisualCenter = getDragViewVisualCenter(d.x, d.y, d.xOffset, d.yOffset,
-            d.dragView, mDragViewVisualCenter);
+        mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
 
         final View child = (mDragInfo == null) ? null : mDragInfo.cell;
         // Identify whether we have dragged over a side page
@@ -3700,7 +3700,7 @@
                     mTargetCell[1]);
 
             manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
-                    targetCellDistance, dragOverView);
+                    targetCellDistance, dragOverView, d.accessibleDrag);
 
             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                     mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
@@ -3738,15 +3738,21 @@
     }
 
     private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout,
-            int[] targetCell, float distance, View dragOverView) {
+            int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) {
         boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance,
                 false);
-
         if (mDragMode == DRAG_MODE_NONE && userFolderPending &&
                 !mFolderCreationAlarm.alarmPending()) {
-            mFolderCreationAlarm.setOnAlarmListener(new
-                    FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]));
-            mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
+
+            FolderCreationAlarmListener listener = new
+                    FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]);
+
+            if (!accessibleDrag) {
+                mFolderCreationAlarm.setOnAlarmListener(listener);
+                mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
+            } else {
+                listener.onAlarm(mFolderCreationAlarm);
+            }
             return;
         }
 
@@ -4043,8 +4049,7 @@
     }
 
     public Bitmap createWidgetBitmap(ItemInfo widgetInfo, View layout) {
-        int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo.spanX,
-                widgetInfo.spanY, widgetInfo, false);
+        int[] unScaledSize = mLauncher.getWorkspace().estimateItemSize(widgetInfo, false);
         int visibility = layout.getVisibility();
         layout.setVisibility(VISIBLE);
 
@@ -4115,7 +4120,6 @@
 
         // In the case where we've prebound the widget, we remove it from the DragLayer
         if (finalView instanceof AppWidgetHostView && external) {
-            Log.d(TAG, "6557954 Animate widget drop, final view is appWidgetHostView");
             mLauncher.getDragLayer().removeView(finalView);
         }
 
@@ -4199,7 +4203,7 @@
      *
      * pixelX and pixelY should be in the coordinate system of layout
      */
-    private int[] findNearestArea(int pixelX, int pixelY,
+    @Thunk int[] findNearestArea(int pixelX, int pixelY,
             int spanX, int spanY, CellLayout layout, int[] recycle) {
         return layout.findNearestArea(
                 pixelX, pixelY, spanX, spanY, recycle);
@@ -4236,19 +4240,14 @@
                 removeWorkspaceItem(mDragInfo.cell);
             }
         } else if (mDragInfo != null) {
-            CellLayout cellLayout;
-            if (mLauncher.isHotseatLayout(target)) {
-                cellLayout = mLauncher.getHotseat().getLayout();
-            } else {
-                cellLayout = getScreenWithId(mDragInfo.screenId);
-            }
-            if (cellLayout == null && LauncherAppState.isDogfoodBuild()) {
-                throw new RuntimeException("Invalid state: cellLayout == null in "
-                        + "Workspace#onDropCompleted. Please file a bug. ");
-            }
+            final CellLayout cellLayout = mLauncher.getCellLayout(
+                    mDragInfo.container, mDragInfo.screenId);
             if (cellLayout != null) {
                 cellLayout.onDropChild(mDragInfo.cell);
-            }
+            } else if (LauncherAppState.isDogfoodBuild()) {
+                throw new RuntimeException("Invalid state: cellLayout == null in "
+                        + "Workspace#onDropCompleted. Please file a bug. ");
+            };
         }
         if ((d.cancelled || (beingCalledAfterUninstall && !mUninstallSuccessful))
                 && mDragInfo.cell != null) {
@@ -4258,6 +4257,9 @@
         mDragInfo = null;
     }
 
+    /**
+     * For opposite operation. See {@link #addInScreen}.
+     */
     public void removeWorkspaceItem(View v) {
         CellLayout parentCell = getParentCellLayoutForView(v);
         if (parentCell != null) {
@@ -4270,11 +4272,13 @@
         }
     }
 
+    @Override
     public void deferCompleteDropAfterUninstallActivity() {
         mDeferDropAfterUninstall = true;
     }
 
     /// maybe move this into a smaller part
+    @Override
     public void onUninstallActivityReturned(boolean success) {
         mDeferDropAfterUninstall = false;
         mUninstallSuccessful = success;
@@ -4306,88 +4310,6 @@
         }
     }
 
-    ArrayList<ComponentName> getUniqueComponents(boolean stripDuplicates, ArrayList<ComponentName> duplicates) {
-        ArrayList<ComponentName> uniqueIntents = new ArrayList<ComponentName>();
-        getUniqueIntents((CellLayout) mLauncher.getHotseat().getLayout(), uniqueIntents, duplicates, false);
-        int count = getChildCount();
-        for (int i = 0; i < count; i++) {
-            CellLayout cl = (CellLayout) getChildAt(i);
-            getUniqueIntents(cl, uniqueIntents, duplicates, false);
-        }
-        return uniqueIntents;
-    }
-
-    void getUniqueIntents(CellLayout cl, ArrayList<ComponentName> uniqueIntents,
-            ArrayList<ComponentName> duplicates, boolean stripDuplicates) {
-        int count = cl.getShortcutsAndWidgets().getChildCount();
-
-        ArrayList<View> children = new ArrayList<View>();
-        for (int i = 0; i < count; i++) {
-            View v = cl.getShortcutsAndWidgets().getChildAt(i);
-            children.add(v);
-        }
-
-        for (int i = 0; i < count; i++) {
-            View v = children.get(i);
-            ItemInfo info = (ItemInfo) v.getTag();
-            // Null check required as the AllApps button doesn't have an item info
-            if (info instanceof ShortcutInfo) {
-                ShortcutInfo si = (ShortcutInfo) info;
-                ComponentName cn = si.intent.getComponent();
-
-                Uri dataUri = si.intent.getData();
-                // If dataUri is not null / empty or if this component isn't one that would
-                // have previously showed up in the AllApps list, then this is a widget-type
-                // shortcut, so ignore it.
-                if (dataUri != null && !dataUri.equals(Uri.EMPTY)) {
-                    continue;
-                }
-
-                if (!uniqueIntents.contains(cn)) {
-                    uniqueIntents.add(cn);
-                } else {
-                    if (stripDuplicates) {
-                        cl.removeViewInLayout(v);
-                        LauncherModel.deleteItemFromDatabase(mLauncher, si);
-                    }
-                    if (duplicates != null) {
-                        duplicates.add(cn);
-                    }
-                }
-            }
-            if (v instanceof FolderIcon) {
-                FolderIcon fi = (FolderIcon) v;
-                ArrayList<View> items = fi.getFolder().getItemsInReadingOrder();
-                for (int j = 0; j < items.size(); j++) {
-                    if (items.get(j).getTag() instanceof ShortcutInfo) {
-                        ShortcutInfo si = (ShortcutInfo) items.get(j).getTag();
-                        ComponentName cn = si.intent.getComponent();
-
-                        Uri dataUri = si.intent.getData();
-                        // If dataUri is not null / empty or if this component isn't one that would
-                        // have previously showed up in the AllApps list, then this is a widget-type
-                        // shortcut, so ignore it.
-                        if (dataUri != null && !dataUri.equals(Uri.EMPTY)) {
-                            continue;
-                        }
-
-                        if (!uniqueIntents.contains(cn)) {
-                            uniqueIntents.add(cn);
-                        }  else {
-                            if (stripDuplicates) {
-                                fi.getFolderInfo().remove(si);
-                                LauncherModel.deleteItemFromDatabase(mLauncher, si);
-                            }
-                            if (duplicates != null) {
-                                duplicates.add(cn);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
     void saveWorkspaceToDb() {
         saveWorkspaceScreenToDb((CellLayout) mLauncher.getHotseat().getLayout());
         int count = getChildCount();
@@ -4420,8 +4342,7 @@
                     cellX = hotseat.getCellXFromOrder((int) info.screenId);
                     cellY = hotseat.getCellYFromOrder((int) info.screenId);
                 }
-                LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX,
-                        cellY, false);
+                LauncherModel.addItemToDatabase(mLauncher, info, container, screenId, cellX, cellY);
             }
             if (v instanceof FolderIcon) {
                 FolderIcon fi = (FolderIcon) v;
@@ -4879,7 +4800,7 @@
                         updates.contains(info)) {
                     ShortcutInfo si = (ShortcutInfo) info;
                     BubbleTextView shortcut = (BubbleTextView) v;
-                    boolean oldPromiseState = shortcut.getCompoundDrawables()[1]
+                    boolean oldPromiseState = getTextViewIcon(shortcut)
                             instanceof PreloadIconDrawable;
                     shortcut.applyFromShortcutInfo(si, mIconCache, true,
                             si.isPromise() != oldPromiseState);
@@ -4914,7 +4835,8 @@
                         if (shortcutInfo.hasStatusFlag(ShortcutInfo.FLAG_AUTOINTALL_ICON)) {
                             // For auto install apps update the icon as well as label.
                             mIconCache.getTitleAndIcon(shortcutInfo,
-                                    shortcutInfo.promisedIntent, user, true);
+                                    shortcutInfo.promisedIntent, user,
+                                    shortcutInfo.shouldUseLowResIcon());
                         } else {
                             // Only update the icon for restored apps.
                             shortcutInfo.updateIcon(mIconCache);
diff --git a/src/com/android/launcher3/compat/AlphabeticIndexCompat.java b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
new file mode 100644
index 0000000..f890706
--- /dev/null
+++ b/src/com/android/launcher3/compat/AlphabeticIndexCompat.java
@@ -0,0 +1,149 @@
+package com.android.launcher3.compat;
+
+import android.content.Context;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+/**
+ * Fallback class to support Alphabetic indexing if not supported by the framework.
+ * TODO(winsonc): disable for non-english locales
+ */
+class BaseAlphabeticIndex {
+
+    private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-";
+    private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1;
+
+    public BaseAlphabeticIndex() {}
+
+    /**
+     * Sets the max number of the label buckets in this index.
+     */
+    public void setMaxLabelCount(int count) {
+        // Not currently supported
+    }
+
+    /**
+     * Returns the index of the bucket in which the given string should appear.
+     */
+    protected int getBucketIndex(String s) {
+        if (s.isEmpty()) {
+            return UNKNOWN_BUCKET_INDEX;
+        }
+        int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase());
+        if (index != -1) {
+            return index;
+        }
+        return UNKNOWN_BUCKET_INDEX;
+    }
+
+    /**
+     * Returns the label for the bucket at the given index (as returned by getBucketIndex).
+     */
+    protected String getBucketLabel(int index) {
+        return BUCKETS.substring(index, index + 1);
+    }
+}
+
+/**
+ * Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index.
+ */
+public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
+
+    private Object mAlphabeticIndex;
+    private Method mAddLabelsMethod;
+    private Method mSetMaxLabelCountMethod;
+    private Method mGetBucketIndexMethod;
+    private Method mGetBucketLabelMethod;
+    private boolean mHasValidAlphabeticIndex;
+
+    public AlphabeticIndexCompat(Context context) {
+        super();
+        try {
+            Locale curLocale = context.getResources().getConfiguration().locale;
+            Class clazz = Class.forName("libcore.icu.AlphabeticIndex");
+            Constructor ctor = clazz.getConstructor(Locale.class);
+            mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class);
+            mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class);
+            mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class);
+            mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class);
+            mAlphabeticIndex = ctor.newInstance(curLocale);
+            try {
+                // Ensure we always have some base English locale buckets
+                if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) {
+                    mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            mHasValidAlphabeticIndex = true;
+        } catch (Exception e) {
+            mHasValidAlphabeticIndex = false;
+        }
+    }
+
+    /**
+     * Sets the max number of the label buckets in this index.
+     * (ICU 51 default is 99)
+     */
+    public void setMaxLabelCount(int count) {
+        if (mHasValidAlphabeticIndex) {
+            try {
+                mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        } else {
+            super.setMaxLabelCount(count);
+        }
+    }
+
+    /**
+     * Computes the section name for an given string {@param s}.
+     */
+    public String computeSectionName(String s) {
+        String sectionName = getBucketLabel(getBucketIndex(s));
+        if (sectionName.trim().isEmpty() && s.length() > 0) {
+            boolean startsWithDigit = Character.isDigit(Character.codePointAt(s.trim(), 0));
+            if (startsWithDigit) {
+                // Digit section
+                return "#";
+            } else {
+                // Unknown section
+                return "\u2022";
+            }
+        }
+        return sectionName;
+    }
+
+    /**
+     * Returns the index of the bucket in which {@param s} should appear.
+     * Function is synchronized because underlying routine walks an iterator
+     * whose state is maintained inside the index object.
+     */
+    protected int getBucketIndex(String s) {
+        if (mHasValidAlphabeticIndex) {
+            try {
+                return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return super.getBucketIndex(s);
+    }
+
+    /**
+     * Returns the label for the bucket at the given index (as returned by getBucketIndex).
+     */
+    protected String getBucketLabel(int index) {
+        if (mHasValidAlphabeticIndex) {
+            try {
+                return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return super.getBucketLabel(index);
+    }
+}
diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java
index 90a4d1a..07ef0ef 100644
--- a/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java
+++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompat.java
@@ -17,7 +17,9 @@
 package com.android.launcher3.compat;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
 
 public abstract class LauncherActivityInfoCompat {
@@ -32,4 +34,11 @@
     public abstract ApplicationInfo getApplicationInfo();
     public abstract long getFirstInstallTime();
     public abstract Drawable getBadgedIcon(int density);
+
+    /**
+     * Creates a LauncherActivityInfoCompat for the primary user.
+     */
+    public static LauncherActivityInfoCompat fromResolveInfo(ResolveInfo info, Context context) {
+        return new LauncherActivityInfoCompatV16(context, info);
+    }
 }
diff --git a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java
index 1d41a6f..ea51aac 100644
--- a/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java
+++ b/src/com/android/launcher3/compat/LauncherActivityInfoCompatV16.java
@@ -29,13 +29,15 @@
 
 
 public class LauncherActivityInfoCompatV16 extends LauncherActivityInfoCompat {
-    private ActivityInfo mActivityInfo;
-    private ComponentName mComponentName;
-    private PackageManager mPm;
+    private final ResolveInfo mResolveInfo;
+    private final ActivityInfo mActivityInfo;
+    private final ComponentName mComponentName;
+    private final PackageManager mPm;
 
     LauncherActivityInfoCompatV16(Context context, ResolveInfo info) {
         super();
-        this.mActivityInfo = info.activityInfo;
+        mResolveInfo = info;
+        mActivityInfo = info.activityInfo;
         mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name);
         mPm = context.getPackageManager();
     }
@@ -49,31 +51,30 @@
     }
 
     public CharSequence getLabel() {
-        return mActivityInfo.loadLabel(mPm);
+        return mResolveInfo.loadLabel(mPm);
     }
 
     public Drawable getIcon(int density) {
-        Drawable d = null;
-        if (mActivityInfo.getIconResource() != 0) {
-            Resources resources;
+        int iconRes = mResolveInfo.getIconResource();
+        Resources resources = null;
+        Drawable icon = null;
+        // Get the preferred density icon from the app's resources
+        if (density != 0 && iconRes != 0) {
             try {
-                resources = mPm.getResourcesForApplication(mActivityInfo.packageName);
-            } catch (PackageManager.NameNotFoundException e) {
-                resources = null;
-            }
-            if (resources != null) {
-                try {
-                    d = resources.getDrawableForDensity(mActivityInfo.getIconResource(), density);
-                } catch (Resources.NotFoundException e) {
-                    // Return default icon below.
-                }
+                resources = mPm.getResourcesForApplication(mActivityInfo.applicationInfo);
+                icon = resources.getDrawableForDensity(iconRes, density);
+            } catch (NameNotFoundException | Resources.NotFoundException exc) {
             }
         }
-        if (d == null) {
-            Resources resources = Resources.getSystem();
-            d = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density);
+        // Get the default density icon
+        if (icon == null) {
+            icon = mResolveInfo.loadIcon(mPm);
         }
-        return d;
+        if (icon == null) {
+            resources = Resources.getSystem();
+            icon = resources.getDrawableForDensity(android.R.mipmap.sym_def_app_icon, density);
+        }
+        return icon;
     }
 
     public ApplicationInfo getApplicationInfo() {
diff --git a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
index e47b9a5..ac3d252 100644
--- a/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
+++ b/src/com/android/launcher3/compat/LauncherAppsCompatV16.java
@@ -31,6 +31,8 @@
 import android.os.Bundle;
 import android.provider.Settings;
 
+import com.android.launcher3.util.Thunk;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -139,11 +141,11 @@
         mContext.registerReceiver(mPackageMonitor, filter);
     }
 
-    private synchronized List<OnAppsChangedCallbackCompat> getCallbacks() {
+    @Thunk synchronized List<OnAppsChangedCallbackCompat> getCallbacks() {
         return new ArrayList<OnAppsChangedCallbackCompat>(mCallbacks);
     }
 
-    private class PackageMonitor extends BroadcastReceiver {
+    @Thunk class PackageMonitor extends BroadcastReceiver {
         public void onReceive(Context context, Intent intent) {
             final String action = intent.getAction();
             final UserHandleCompat user = UserHandleCompat.myUserHandle();
diff --git a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
index 601f04c..d6d4b82 100644
--- a/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
+++ b/src/com/android/launcher3/compat/PackageInstallerCompatVL.java
@@ -26,6 +26,7 @@
 
 import com.android.launcher3.IconCache;
 import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -36,10 +37,10 @@
     private static final boolean DEBUG = false;
 
     // All updates to these sets must happen on the {@link #mWorker} thread.
-    private final SparseArray<SessionInfo> mPendingReplays = new SparseArray<SessionInfo>();
-    private final HashSet<String> mPendingBadgeUpdates = new HashSet<String>();
+    @Thunk final SparseArray<SessionInfo> mPendingReplays = new SparseArray<SessionInfo>();
+    @Thunk final HashSet<String> mPendingBadgeUpdates = new HashSet<String>();
 
-    private final PackageInstaller mInstaller;
+    @Thunk final PackageInstaller mInstaller;
     private final IconCache mCache;
     private final Handler mWorker;
 
@@ -82,7 +83,7 @@
         return activePackages;
     }
 
-    private void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) {
+    @Thunk void addSessionInfoToCahce(SessionInfo info, UserHandleCompat user) {
         String packageName = info.getAppPackageName();
         if (packageName != null) {
             mCache.cachePackageInstallInfo(packageName, user, info.getAppIcon(),
@@ -123,7 +124,7 @@
         replayUpdates(null);
     }
 
-    private void replayUpdates(PackageInstallInfo newInfo) {
+    @Thunk void replayUpdates(PackageInstallInfo newInfo) {
         if (DEBUG) Log.d(TAG, "updates resumed");
         if (!mResumed || !mBound) {
             // Not yet ready
diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java
index 1374b4e..a79d946 100644
--- a/src/com/android/launcher3/compat/UserManagerCompat.java
+++ b/src/com/android/launcher3/compat/UserManagerCompat.java
@@ -43,4 +43,5 @@
     public abstract UserHandleCompat getUserForSerialNumber(long serialNumber);
     public abstract Drawable getBadgedDrawableForUser(Drawable unbadged, UserHandleCompat user);
     public abstract CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user);
+    public abstract long getUserCreationTime(UserHandleCompat user);
 }
diff --git a/src/com/android/launcher3/compat/UserManagerCompatV16.java b/src/com/android/launcher3/compat/UserManagerCompatV16.java
index 32f972e..ffe698c 100644
--- a/src/com/android/launcher3/compat/UserManagerCompatV16.java
+++ b/src/com/android/launcher3/compat/UserManagerCompatV16.java
@@ -48,4 +48,9 @@
     public CharSequence getBadgedLabelForUser(CharSequence label, UserHandleCompat user) {
         return label;
     }
+
+    @Override
+    public long getUserCreationTime(UserHandleCompat user) {
+        return 0;
+    }
 }
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java
index 19eeabd..884d6fe 100644
--- a/src/com/android/launcher3/compat/UserManagerCompatVL.java
+++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java
@@ -18,21 +18,27 @@
 package com.android.launcher3.compat;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.os.UserHandle;
-import android.os.UserManager;
+
+import com.android.launcher3.LauncherAppState;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 public class UserManagerCompatVL extends UserManagerCompatV17 {
+    private static final String USER_CREATION_TIME_KEY = "user_creation_time_";
+
     private final PackageManager mPm;
+    private final Context mContext;
 
     UserManagerCompatVL(Context context) {
         super(context);
         mPm = context.getPackageManager();
+        mContext = context;
     }
 
     @Override
@@ -61,5 +67,17 @@
         }
         return mPm.getUserBadgedLabel(label, user.getUser());
     }
+
+    @Override
+    public long getUserCreationTime(UserHandleCompat user) {
+        // TODO: Use system API once available.
+        SharedPreferences prefs = mContext.getSharedPreferences(
+                LauncherAppState.getSharedPreferencesKey(), Context.MODE_PRIVATE);
+        String key = USER_CREATION_TIME_KEY + getSerialNumberForUser(user);
+        if (!prefs.contains(key)) {
+            prefs.edit().putLong(key, System.currentTimeMillis()).apply();
+        }
+        return prefs.getLong(key, 0);
+    }
 }
 
diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java
new file mode 100644
index 0000000..a84e7df
--- /dev/null
+++ b/src/com/android/launcher3/util/FocusLogic.java
@@ -0,0 +1,508 @@
+/*
+ * 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.launcher3.util;
+
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+
+import java.util.Arrays;
+
+/**
+ * Calculates the next item that a {@link KeyEvent} should change the focus to.
+ *<p>
+ * Note, this utility class calculates everything regards to icon index and its (x,y) coordinates.
+ * Currently supports:
+ * <ul>
+ *  <li> full matrix of cells that are 1x1
+ *  <li> sparse matrix of cells that are 1x1
+ *     [ 1][  ][ 2][  ]
+ *     [  ][  ][ 3][  ]
+ *     [  ][ 4][  ][  ]
+ *     [  ][ 5][ 6][ 7]
+ * </ul>
+ * *<p>
+ * For testing, one can use a BT keyboard, or use following adb command.
+ * ex. $ adb shell input keyevent 20 // KEYCODE_DPAD_LEFT
+ */
+public class FocusLogic {
+
+    private static final String TAG = "FocusLogic";
+    private static final boolean DEBUG = false;
+
+    // Item and page index related constant used by {@link #handleKeyEvent}.
+    public static final int NOOP = -1;
+
+    public static final int PREVIOUS_PAGE_RIGHT_COLUMN  = -2;
+    public static final int PREVIOUS_PAGE_FIRST_ITEM    = -3;
+    public static final int PREVIOUS_PAGE_LAST_ITEM     = -4;
+    public static final int PREVIOUS_PAGE_LEFT_COLUMN   = -5;
+
+    public static final int CURRENT_PAGE_FIRST_ITEM     = -6;
+    public static final int CURRENT_PAGE_LAST_ITEM      = -7;
+
+    public static final int NEXT_PAGE_FIRST_ITEM        = -8;
+    public static final int NEXT_PAGE_LEFT_COLUMN       = -9;
+    public static final int NEXT_PAGE_RIGHT_COLUMN      = -10;
+
+    // Matrix related constant.
+    public static final int EMPTY = -1;
+    public static final int PIVOT = 100;
+
+    /**
+     * Returns true only if this utility class handles the key code.
+     */
+    public static boolean shouldConsume(int keyCode) {
+        return (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
+                keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
+                keyCode == KeyEvent.KEYCODE_MOVE_HOME || keyCode == KeyEvent.KEYCODE_MOVE_END ||
+                keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN ||
+                keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL);
+    }
+
+    public static int handleKeyEvent(int keyCode, int cntX, int cntY, int [][] map,
+            int iconIdx, int pageIndex, int pageCount) {
+
+        if (DEBUG) {
+            Log.v(TAG, String.format(
+                    "handleKeyEvent START: cntX=%d, cntY=%d, iconIdx=%d, pageIdx=%d, pageCnt=%d",
+                    cntX, cntY, iconIdx, pageIndex, pageCount));
+        }
+
+        DeviceProfile profile = LauncherAppState.getInstance().getDynamicGrid()
+                .getDeviceProfile();
+        int newIndex = NOOP;
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/);
+                if (!profile.isLayoutRtl && newIndex == NOOP && pageIndex > 0) {
+                    newIndex = PREVIOUS_PAGE_RIGHT_COLUMN;
+                } else if (profile.isLayoutRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
+                    newIndex = NEXT_PAGE_RIGHT_COLUMN;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/);
+                if (!profile.isLayoutRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
+                    newIndex = NEXT_PAGE_LEFT_COLUMN;
+                } else if (profile.isLayoutRtl && newIndex == NOOP && pageIndex > 0) {
+                    newIndex = PREVIOUS_PAGE_LEFT_COLUMN;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, 1  /*increment*/);
+                break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                newIndex = handleDpadVertical(iconIdx, cntX, cntY, map, -1  /*increment*/);
+                break;
+            case KeyEvent.KEYCODE_MOVE_HOME:
+                newIndex = handleMoveHome();
+                break;
+            case KeyEvent.KEYCODE_MOVE_END:
+                newIndex = handleMoveEnd();
+                break;
+            case KeyEvent.KEYCODE_PAGE_DOWN:
+                newIndex = handlePageDown(pageIndex, pageCount);
+                break;
+            case KeyEvent.KEYCODE_PAGE_UP:
+                newIndex = handlePageUp(pageIndex);
+                break;
+            default:
+                break;
+        }
+
+        if (DEBUG) {
+            Log.v(TAG, String.format("handleKeyEvent FINISH: index [%d -> %s]",
+                    iconIdx, getStringIndex(newIndex)));
+        }
+        return newIndex;
+    }
+
+    /**
+     * Returns a matrix of size (m x n) that has been initialized with {@link #EMPTY}.
+     *
+     * @param m                 number of columns in the matrix
+     * @param n                 number of rows in the matrix
+     */
+    // TODO: get rid of dynamic matrix creation.
+    private static int[][] createFullMatrix(int m, int n) {
+        int[][] matrix = new int [m][n];
+
+        for (int i=0; i < m;i++) {
+            Arrays.fill(matrix[i], EMPTY);
+        }
+        return matrix;
+    }
+
+    /**
+     * Returns a matrix of size same as the {@link CellLayout} dimension that is initialized with the
+     * index of the child view.
+     */
+    // TODO: get rid of the dynamic matrix creation
+    public static int[][] createSparseMatrix(CellLayout layout) {
+        ShortcutAndWidgetContainer parent = layout.getShortcutsAndWidgets();
+        final int m = layout.getCountX();
+        final int n = layout.getCountY();
+        final boolean invert = parent.invertLayoutHorizontally();
+
+        int[][] matrix = createFullMatrix(m, n);
+
+        // Iterate thru the children.
+        for (int i = 0; i < parent.getChildCount(); i++ ) {
+            int cx = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellX;
+            int cy = ((CellLayout.LayoutParams) parent.getChildAt(i).getLayoutParams()).cellY;
+            matrix[invert ? (m - cx - 1) : cx][cy] = i;
+        }
+        if (DEBUG) {
+            printMatrix(matrix);
+        }
+        return matrix;
+    }
+
+    /**
+     * Creates a sparse matrix that merges the icon and hotseat view group using the cell layout.
+     * The size of the returning matrix is [icon column count x (icon + hotseat row count)]
+     * in portrait orientation. In landscape, [(icon + hotseat) column count x (icon row count)]
+     */
+    // TODO: get rid of the dynamic matrix creation
+    public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout,
+            boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) {
+
+        ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
+        ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets();
+
+        int m, n;
+        if (isHorizontal) {
+            m = iconLayout.getCountX();
+            n = iconLayout.getCountY() + hotseatLayout.getCountY();
+        } else {
+            m = iconLayout.getCountX() + hotseatLayout.getCountX();
+            n = iconLayout.getCountY();
+        }
+        int[][] matrix = createFullMatrix(m, n);
+
+        // Iterate thru the children of the top parent.
+        for (int i = 0; i < iconParent.getChildCount(); i++) {
+            int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX;
+            int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY;
+            matrix[cx][cy] = i;
+        }
+
+        // Iterate thru the children of the bottom parent
+        // The hotseat view group contains one more item than iconLayout column count.
+        // If {@param allappsiconRank} not negative, then the last icon in the hotseat
+        // is truncated. If it is negative, then all apps icon index is not inserted.
+        for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) {
+            int delta = 0;
+            if (isHorizontal) {
+                int cx = ((CellLayout.LayoutParams)
+                        hotseatParent.getChildAt(i).getLayoutParams()).cellX;
+                if ((includeAllappsicon && cx >= allappsiconRank) ||
+                        (!includeAllappsicon && cx > allappsiconRank)) {
+                        delta = -1;
+                }
+                matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i;
+            } else {
+                int cy = ((CellLayout.LayoutParams)
+                        hotseatParent.getChildAt(i).getLayoutParams()).cellY;
+                if ((includeAllappsicon && cy >= allappsiconRank) ||
+                        (!includeAllappsicon && cy > allappsiconRank)) {
+                        delta = -1;
+                }
+                matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i;
+            }
+        }
+        if (DEBUG) {
+            printMatrix(matrix);
+        }
+        return matrix;
+    }
+
+    /**
+     * Creates a sparse matrix that merges the icon of previous/next page and last column of
+     * current page. When left key is triggered on the leftmost column, sparse matrix is created
+     * that combines previous page matrix and an extra column on the right. Likewise, when right
+     * key is triggered on the rightmost column, sparse matrix is created that combines this column
+     * on the 0th column and the next page matrix.
+     *
+     * @param pivotX    x coordinate of the focused item in the current page
+     * @param pivotY    y coordinate of the focused item in the current page
+     */
+    // TODO: get rid of the dynamic matrix creation
+    public static int[][] createSparseMatrix(CellLayout iconLayout, int pivotX, int pivotY) {
+
+        ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
+
+        int[][] matrix = createFullMatrix(iconLayout.getCountX() + 1, iconLayout.getCountY());
+
+        // Iterate thru the children of the top parent.
+        for (int i = 0; i < iconParent.getChildCount(); i++) {
+            int cx = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellX;
+            int cy = ((CellLayout.LayoutParams) iconParent.getChildAt(i).getLayoutParams()).cellY;
+            if (pivotX < 0) {
+                matrix[cx - pivotX][cy] = i;
+            } else {
+                matrix[cx][cy] = i;
+            }
+        }
+
+        if (pivotX < 0) {
+            matrix[0][pivotY] = PIVOT;
+        } else {
+            matrix[pivotX][pivotY] = PIVOT;
+        }
+        if (DEBUG) {
+            printMatrix(matrix);
+        }
+        return matrix;
+    }
+
+    //
+    // key event handling methods.
+    //
+
+    /**
+     * Calculates icon that has is closest to the horizontal axis in reference to the cur icon.
+     *
+     * Example of the check order for KEYCODE_DPAD_RIGHT:
+     * [  ][  ][13][14][15]
+     * [  ][ 6][ 8][10][12]
+     * [ X][ 1][ 2][ 3][ 4]
+     * [  ][ 5][ 7][ 9][11]
+     */
+    // TODO: add unit tests to verify all permutation.
+    private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY,
+            int[][] matrix, int increment) {
+        if(matrix == null) {
+            throw new IllegalStateException("Dpad navigation requires a matrix.");
+        }
+        int newIconIndex = NOOP;
+
+        int xPos = -1;
+        int yPos = -1;
+        // Figure out the location of the icon.
+        for (int i = 0; i < cntX; i++) {
+            for (int j = 0; j < cntY; j++) {
+                if (matrix[i][j] == iconIdx) {
+                    xPos = i;
+                    yPos = j;
+                }
+            }
+        }
+        if (DEBUG) {
+            Log.v(TAG, String.format("\thandleDpadHorizontal: \t[x, y]=[%d, %d] iconIndex=%d",
+                    xPos, yPos, iconIdx));
+        }
+
+        // Rule1: check first in the horizontal direction
+        for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) {
+            if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) {
+                return newIconIndex;
+            }
+        }
+
+        // Rule2: check (x1-n, yPos + increment),   (x1-n, yPos - increment)
+        //              (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment)
+        int nextYPos1;
+        int nextYPos2;
+        int i = -1;
+        for (int coeff = 1; coeff < cntY; coeff++) {
+            nextYPos1 = yPos + coeff * increment;
+            nextYPos2 = yPos - coeff * increment;
+            for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) {
+                if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) {
+                    return newIconIndex;
+                }
+                if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) {
+                    return newIconIndex;
+                }
+            }
+        }
+        return newIconIndex;
+    }
+
+    /**
+     * Calculates icon that is closest to the vertical axis in reference to the current icon.
+     *
+     * Example of the check order for KEYCODE_DPAD_DOWN:
+     * [  ][  ][  ][ X][  ][  ][  ]
+     * [  ][  ][ 5][ 1][ 4][  ][  ]
+     * [  ][10][ 7][ 2][ 6][ 9][  ]
+     * [14][12][ 9][ 3][ 8][11][13]
+     */
+    // TODO: add unit tests to verify all permutation.
+    private static int handleDpadVertical(int iconIndex, int cntX, int cntY,
+            int [][] matrix, int increment) {
+        int newIconIndex = NOOP;
+        if(matrix == null) {
+            throw new IllegalStateException("Dpad navigation requires a matrix.");
+        }
+
+        int xPos = -1;
+        int yPos = -1;
+        // Figure out the location of the icon.
+        for (int i = 0; i< cntX; i++) {
+            for (int j = 0; j < cntY; j++) {
+                if (matrix[i][j] == iconIndex) {
+                    xPos = i;
+                    yPos = j;
+                }
+            }
+        }
+
+        if (DEBUG) {
+            Log.v(TAG, String.format("\thandleDpadVertical: \t[x, y]=[%d, %d] iconIndex=%d",
+                    xPos, yPos, iconIndex));
+        }
+
+        // Rule1: check first in the dpad direction
+        for (int j = yPos + increment; 0 <= j && j <cntY && 0 <= j; j = j + increment) {
+            if ((newIconIndex = inspectMatrix(xPos, j, cntX, cntY, matrix)) != NOOP) {
+                return newIconIndex;
+            }
+        }
+
+        // Rule2: check (xPos + increment, y_(1-n)),   (xPos - increment, y_(1-n))
+        //              (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n))
+        int nextXPos1;
+        int nextXPos2;
+        int j = -1;
+        for (int coeff = 1; coeff < cntX; coeff++) {
+            nextXPos1 = xPos + coeff * increment;
+            nextXPos2 = xPos - coeff * increment;
+            for (j = yPos + increment * coeff; 0 <= j && j < cntY; j = j + increment) {
+                if ((newIconIndex = inspectMatrix(nextXPos1, j, cntX, cntY, matrix)) != NOOP) {
+                    return newIconIndex;
+                }
+                if ((newIconIndex = inspectMatrix(nextXPos2, j, cntX, cntY, matrix)) != NOOP) {
+                    return newIconIndex;
+                }
+            }
+        }
+        return newIconIndex;
+    }
+
+    private static int handleMoveHome() {
+        return CURRENT_PAGE_FIRST_ITEM;
+    }
+
+    private static int handleMoveEnd() {
+        return CURRENT_PAGE_LAST_ITEM;
+    }
+
+    private static int handlePageDown(int pageIndex, int pageCount) {
+        if (pageIndex < pageCount -1) {
+            return NEXT_PAGE_FIRST_ITEM;
+        }
+        return CURRENT_PAGE_LAST_ITEM;
+    }
+
+    private static int handlePageUp(int pageIndex) {
+        if (pageIndex > 0) {
+            return PREVIOUS_PAGE_FIRST_ITEM;
+        } else {
+            return CURRENT_PAGE_FIRST_ITEM;
+        }
+    }
+
+    //
+    // Helper methods.
+    //
+
+    private static boolean isValid(int xPos, int yPos, int countX, int countY) {
+        return (0 <= xPos && xPos < countX && 0 <= yPos && yPos < countY);
+    }
+
+    private static int inspectMatrix(int x, int y, int cntX, int cntY, int[][] matrix) {
+        int newIconIndex = NOOP;
+        if (isValid(x, y, cntX, cntY)) {
+            if (matrix[x][y] != -1) {
+                newIconIndex = matrix[x][y];
+                if (DEBUG) {
+                    Log.v(TAG, String.format("\t\tinspect: \t[x, y]=[%d, %d] %d",
+                            x, y, matrix[x][y]));
+                }
+                return newIconIndex;
+            }
+        }
+        return newIconIndex;
+    }
+
+    /**
+     * Only used for debugging.
+     */
+    private static String getStringIndex(int index) {
+        switch(index) {
+            case NOOP: return "NOOP";
+            case PREVIOUS_PAGE_FIRST_ITEM:  return "PREVIOUS_PAGE_FIRST";
+            case PREVIOUS_PAGE_LAST_ITEM:   return "PREVIOUS_PAGE_LAST";
+            case PREVIOUS_PAGE_RIGHT_COLUMN:return "PREVIOUS_PAGE_RIGHT_COLUMN";
+            case CURRENT_PAGE_FIRST_ITEM:   return "CURRENT_PAGE_FIRST";
+            case CURRENT_PAGE_LAST_ITEM:    return "CURRENT_PAGE_LAST";
+            case NEXT_PAGE_FIRST_ITEM:      return "NEXT_PAGE_FIRST";
+            case NEXT_PAGE_LEFT_COLUMN:     return "NEXT_PAGE_LEFT_COLUMN";
+            default:
+                return Integer.toString(index);
+        }
+    }
+
+    /**
+     * Only used for debugging.
+     */
+    private static void printMatrix(int[][] matrix) {
+        Log.v(TAG, "\tprintMap:");
+        int m = matrix.length;
+        int n = matrix[0].length;
+
+        for (int j=0; j < n; j++) {
+            String colY = "\t\t";
+            for (int i=0; i < m; i++) {
+                colY +=  String.format("%3d",matrix[i][j]);
+            }
+            Log.v(TAG, colY);
+        }
+    }
+
+    /**
+     * @param edgeColumn the column of the new icon. either {@link #NEXT_PAGE_LEFT_COLUMN} or
+     * {@link #NEXT_PAGE_RIGHT_COLUMN}
+     * @return the view adjacent to {@param oldView} in the {@param nextPage}.
+     */
+    public static View getAdjacentChildInNextPage(
+            ShortcutAndWidgetContainer nextPage, View oldView, int edgeColumn) {
+        final int newRow = ((CellLayout.LayoutParams) oldView.getLayoutParams()).cellY;
+
+        int column = (edgeColumn == NEXT_PAGE_LEFT_COLUMN) ^ nextPage.invertLayoutHorizontally()
+                ? 0 : (((CellLayout) nextPage.getParent()).getCountX() - 1);
+
+        for (; column >= 0; column--) {
+            for (int row = newRow; row >= 0; row--) {
+                View newView = nextPage.getChildAt(column, row);
+                if (newView != null) {
+                    return newView;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java
new file mode 100644
index 0000000..cefa71c
--- /dev/null
+++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java
@@ -0,0 +1,277 @@
+package com.android.launcher3.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+import com.android.launcher3.FolderInfo;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherFiles;
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.compat.LauncherActivityInfoCompat;
+import com.android.launcher3.compat.LauncherAppsCompat;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Handles addition of app shortcuts for managed profiles.
+ * Methods of class should only be called on {@link LauncherModel#sWorkerThread}.
+ */
+public class ManagedProfileHeuristic {
+
+    private static final String TAG = "ManagedProfileHeuristic";
+
+    /**
+     * Maintain a set of packages installed per user.
+     */
+    private static final String INSTALLED_PACKAGES_PREFIX = "installed_packages_for_user_";
+
+    private static final String USER_FOLDER_ID_PREFIX = "user_folder_";
+
+    /**
+     * Duration (in milliseconds) for which app shortcuts will be added to work folder.
+     */
+    private static final long AUTO_ADD_TO_FOLDER_DURATION = 8 * 60 * 60 * 1000;
+
+    public static ManagedProfileHeuristic get(Context context, UserHandleCompat user) {
+        if (Utilities.isLmpOrAbove() && !UserHandleCompat.myUserHandle().equals(user)) {
+            return new ManagedProfileHeuristic(context, user);
+        }
+        return null;
+    }
+
+    private final Context mContext;
+    private final UserHandleCompat mUser;
+    private final LauncherModel mModel;
+
+    private final SharedPreferences mPrefs;
+    private final long mUserSerial;
+    private final long mUserCreationTime;
+    private final String mPackageSetKey;
+
+    private ArrayList<ItemInfo> mHomescreenApps;
+    private ArrayList<ItemInfo> mWorkFolderApps;
+
+    private ManagedProfileHeuristic(Context context, UserHandleCompat user) {
+        mContext = context;
+        mUser = user;
+        mModel = LauncherAppState.getInstance().getModel();
+
+        UserManagerCompat userManager = UserManagerCompat.getInstance(context);
+        mUserSerial = userManager.getSerialNumberForUser(user);
+        mUserCreationTime = userManager.getUserCreationTime(user);
+        mPackageSetKey = INSTALLED_PACKAGES_PREFIX + mUserSerial;
+
+        mPrefs = mContext.getSharedPreferences(LauncherFiles.MANAGED_USER_PREFERENCES_KEY,
+                Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Checks the list of user apps and adds icons for newly installed apps on the homescreen or
+     * workfolder.
+     */
+    public void processUserApps(List<LauncherActivityInfoCompat> apps) {
+        mHomescreenApps = new ArrayList<ItemInfo>();
+        mWorkFolderApps = new ArrayList<ItemInfo>();
+        HashSet<String> packageSet = getPackageSet();
+        boolean newPackageAdded = false;
+
+        for (LauncherActivityInfoCompat info : apps) {
+            String packageName = info.getComponentName().getPackageName();
+            if (!packageSet.contains(packageName)) {
+                packageSet.add(packageName);
+                newPackageAdded = true;
+
+                try {
+                    PackageInfo pkgInfo = mContext.getPackageManager()
+                            .getPackageInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
+                    markForAddition(info, pkgInfo.firstInstallTime);
+                } catch (NameNotFoundException e) {
+                    Log.e(TAG, "Unknown package " + packageName, e);
+                }
+            }
+        }
+
+        if (newPackageAdded) {
+            mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply();
+            finalizeAdditions();
+        }
+    }
+
+    private void markForAddition(LauncherActivityInfoCompat info, long installTime) {
+        ArrayList<ItemInfo> targetList =
+                (installTime <= mUserCreationTime + AUTO_ADD_TO_FOLDER_DURATION) ?
+                        mWorkFolderApps : mHomescreenApps;
+        targetList.add(ShortcutInfo.fromActivityInfo(info, mContext));
+    }
+
+    /**
+     * Adds and binds shortcuts marked to be added to the work folder.
+     */
+    private void finalizeWorkFolder() {
+        if (mWorkFolderApps.isEmpty()) {
+            return;
+        }
+
+        // Try to get a work folder.
+        String folderIdKey = USER_FOLDER_ID_PREFIX + mUserSerial;
+        if (mPrefs.contains(folderIdKey)) {
+            long folderId = mPrefs.getLong(folderIdKey, 0);
+            final FolderInfo workFolder = mModel.findFolderById(folderId);
+
+            if (workFolder == null || !workFolder.hasOption(FolderInfo.FLAG_WORK_FOLDER)) {
+                // Could not get a work folder. Add all the icons to homescreen.
+                mHomescreenApps.addAll(mWorkFolderApps);
+                return;
+            }
+            saveWorkFolderShortcuts(folderId, workFolder.contents.size());
+
+            final ArrayList<ItemInfo> shortcuts = mWorkFolderApps;
+            // FolderInfo could already be bound. We need to add shortcuts on the UI thread.
+            new MainThreadExecutor().execute(new Runnable() {
+
+                @Override
+                public void run() {
+                    for (ItemInfo info : shortcuts) {
+                        workFolder.add((ShortcutInfo) info);
+                    }
+                }
+            });
+        } else {
+            // Create a new folder.
+            final FolderInfo workFolder = new FolderInfo();
+            workFolder.title = mContext.getText(R.string.work_folder_name);
+            workFolder.setOption(FolderInfo.FLAG_WORK_FOLDER, true, null);
+
+            // Add all shortcuts before adding it to the UI, as an empty folder might get deleted.
+            for (ItemInfo info : mWorkFolderApps) {
+                workFolder.add((ShortcutInfo) info);
+            }
+
+            // Add the item to home screen and DB. This also generates an item id synchronously.
+            ArrayList<ItemInfo> itemList = new ArrayList<ItemInfo>(1);
+            itemList.add(workFolder);
+            mModel.addAndBindAddedWorkspaceItems(mContext, itemList);
+            mPrefs.edit().putLong(USER_FOLDER_ID_PREFIX + mUserSerial, workFolder.id).apply();
+
+            saveWorkFolderShortcuts(workFolder.id, 0);
+        }
+    }
+
+    /**
+     * Add work folder shortcuts to the DB.
+     */
+    private void saveWorkFolderShortcuts(long workFolderId, int startingRank) {
+        for (ItemInfo info : mWorkFolderApps) {
+            info.rank = startingRank++;
+            LauncherModel.addItemToDatabase(mContext, info, workFolderId, 0, 0, 0);
+        }
+    }
+
+    /**
+     * Adds and binds all shortcuts marked for addition.
+     */
+    private void finalizeAdditions() {
+        finalizeWorkFolder();
+
+        if (!mHomescreenApps.isEmpty()) {
+            mModel.addAndBindAddedWorkspaceItems(mContext, mHomescreenApps);
+        }
+    }
+
+    /**
+     * Updates the list of installed apps and adds any new icons on homescreen or work folder.
+     */
+    public void processPackageAdd(String[] packages) {
+        mHomescreenApps = new ArrayList<ItemInfo>();
+        mWorkFolderApps = new ArrayList<ItemInfo>();
+        HashSet<String> packageSet = getPackageSet();
+        boolean newPackageAdded = false;
+        long installTime = System.currentTimeMillis();
+        LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(mContext);
+
+        for (String packageName : packages) {
+            if (!packageSet.contains(packageName)) {
+                packageSet.add(packageName);
+                newPackageAdded = true;
+
+                List<LauncherActivityInfoCompat> activities =
+                        launcherApps.getActivityList(packageName, mUser);
+                if (!activities.isEmpty()) {
+                    markForAddition(activities.get(0), installTime);
+                }
+            }
+        }
+
+        if (newPackageAdded) {
+            mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply();
+            finalizeAdditions();
+        }
+    }
+
+    /**
+     * Updates the list of installed packages for the user.
+     */
+    public void processPackageRemoved(String[] packages) {
+        HashSet<String> packageSet = getPackageSet();
+        boolean packageRemoved = false;
+
+        for (String packageName : packages) {
+            if (packageSet.remove(packageName)) {
+                packageRemoved = true;
+            }
+        }
+
+        if (packageRemoved) {
+            mPrefs.edit().putStringSet(mPackageSetKey, packageSet).apply();
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private HashSet<String> getPackageSet() {
+        return new HashSet<String>(mPrefs.getStringSet(mPackageSetKey, Collections.EMPTY_SET));
+    }
+
+    /**
+     * Verifies that entries corresponding to {@param users} exist and removes all invalid entries.
+     */
+    public static void processAllUsers(List<UserHandleCompat> users, Context context) {
+        if (!Utilities.isLmpOrAbove()) {
+            return;
+        }
+        UserManagerCompat userManager = UserManagerCompat.getInstance(context);
+        HashSet<String> validKeys = new HashSet<String>();
+        for (UserHandleCompat user : users) {
+            addAllUserKeys(userManager.getSerialNumberForUser(user), validKeys);
+        }
+
+        SharedPreferences prefs = context.getSharedPreferences(
+                LauncherFiles.MANAGED_USER_PREFERENCES_KEY,
+                Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = prefs.edit();
+        for (String key : prefs.getAll().keySet()) {
+            if (!validKeys.contains(key)) {
+                editor.remove(key);
+            }
+        }
+        editor.apply();
+    }
+
+    private static void addAllUserKeys(long userSerial, HashSet<String> keysOut) {
+        keysOut.add(INSTALLED_PACKAGES_PREFIX + userSerial);
+        keysOut.add(USER_FOLDER_ID_PREFIX + userSerial);
+    }
+}
diff --git a/src/com/android/launcher3/util/Thunk.java b/src/com/android/launcher3/util/Thunk.java
new file mode 100644
index 0000000..de350b0
--- /dev/null
+++ b/src/com/android/launcher3/util/Thunk.java
@@ -0,0 +1,43 @@
+/*
+ * 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.launcher3.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the given field or method has package visibility solely to prevent the creation
+ * of a synthetic method. In practice, you should treat this field/method as if it were private.
+ * <p>
+ *
+ * When a private method is called from an inner class, the Java compiler generates a simple
+ * package private shim method that the class generated from the inner class can call. This results
+ * in unnecessary bloat and runtime method call overhead. It also gets us closer to the dex method
+ * count limit.
+ * <p>
+ *
+ * If you'd like to see warnings for these synthetic methods in eclipse, turn on:
+ * Window > Preferences > Java > Compiler > Errors/Warnings > "Access to a non-accessible member
+ * of an enclosing type".
+ * <p>
+ *
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE})
+public @interface Thunk { }
\ No newline at end of file
diff --git a/src/com/android/launcher3/util/WallpaperUtils.java b/src/com/android/launcher3/util/WallpaperUtils.java
new file mode 100644
index 0000000..53b2acd
--- /dev/null
+++ b/src/com/android/launcher3/util/WallpaperUtils.java
@@ -0,0 +1,123 @@
+/*
+ * 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.launcher3.util;
+
+import android.annotation.TargetApi;
+import android.app.WallpaperManager;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Build;
+import android.view.WindowManager;
+
+/**
+ * Utility methods for wallpaper management.
+ */
+public final class WallpaperUtils {
+
+    public static final String WALLPAPER_WIDTH_KEY = "wallpaper.width";
+    public static final String WALLPAPER_HEIGHT_KEY = "wallpaper.height";
+    public static final float WALLPAPER_SCREENS_SPAN = 2f;
+
+    public static void suggestWallpaperDimension(Resources res,
+            final SharedPreferences sharedPrefs,
+            WindowManager windowManager,
+            final WallpaperManager wallpaperManager, boolean fallBackToDefaults) {
+        final Point defaultWallpaperSize = WallpaperUtils.getDefaultWallpaperSize(res, windowManager);
+        // If we have saved a wallpaper width/height, use that instead
+
+        int savedWidth = sharedPrefs.getInt(WALLPAPER_WIDTH_KEY, -1);
+        int savedHeight = sharedPrefs.getInt(WALLPAPER_HEIGHT_KEY, -1);
+
+        if (savedWidth == -1 || savedHeight == -1) {
+            if (!fallBackToDefaults) {
+                return;
+            } else {
+                savedWidth = defaultWallpaperSize.x;
+                savedHeight = defaultWallpaperSize.y;
+            }
+        }
+
+        if (savedWidth != wallpaperManager.getDesiredMinimumWidth() ||
+                savedHeight != wallpaperManager.getDesiredMinimumHeight()) {
+            wallpaperManager.suggestDesiredDimensions(savedWidth, savedHeight);
+        }
+    }
+
+    /**
+     * 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;
+    }
+
+    private static Point sDefaultWallpaperSize;
+
+    @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);
+
+            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 (res.getConfiguration().smallestScreenWidthDp >= 720) {
+                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;
+    }
+}
diff --git a/src/com/android/launcher3/widget/PackageItemInfo.java b/src/com/android/launcher3/widget/PackageItemInfo.java
new file mode 100644
index 0000000..1a1de55
--- /dev/null
+++ b/src/com/android/launcher3/widget/PackageItemInfo.java
@@ -0,0 +1,58 @@
+/*
+ * 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.launcher3.widget;
+
+import android.content.ComponentName;
+import android.graphics.Bitmap;
+
+import com.android.launcher3.ItemInfo;
+
+import java.util.Arrays;
+
+/**
+ * Represents a {@link Package} in the widget tray section.
+ */
+public class PackageItemInfo extends ItemInfo {
+    private static final String TAG = "PackageInfo";
+
+    /**
+     * A bitmap version of the application icon.
+     */
+    public Bitmap iconBitmap;
+
+    /**
+     * Indicates whether we're using a low res icon
+     */
+    public boolean usingLowResIcon;
+
+    public String packageName;
+
+    int flags = 0;
+
+    PackageItemInfo(String packageName) {
+        this.packageName = packageName;
+    }
+
+    @Override
+    public String toString() {
+        return "PackageItemInfo(title=" + title.toString() + " id=" + this.id
+                + " type=" + this.itemType + " container=" + this.container
+                + " screen=" + screenId + " cellX=" + cellX + " cellY=" + cellY
+                + " spanX=" + spanX + " spanY=" + spanY + " dropPos=" + Arrays.toString(dropPos)
+                + " user=" + user + ")";
+    }
+}
diff --git a/src/com/android/launcher3/widget/PendingAddShortcutInfo.java b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
new file mode 100644
index 0000000..a569850
--- /dev/null
+++ b/src/com/android/launcher3/widget/PendingAddShortcutInfo.java
@@ -0,0 +1,44 @@
+/*
+ * 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.launcher3.widget;
+
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.PendingAddItemInfo;
+
+/**
+ * Meta data used for late binding of the short cuts.
+ *
+ * @see {@link PendingAddItemInfo}
+ */
+public class PendingAddShortcutInfo extends PendingAddItemInfo {
+
+    ActivityInfo activityInfo;
+
+    public PendingAddShortcutInfo(ActivityInfo activityInfo) {
+        this.activityInfo = activityInfo;
+        componentName = new ComponentName(activityInfo.packageName, activityInfo.name);
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("PendingAddShortcutInfo package=%s, name=%s",
+                activityInfo.packageName, activityInfo.name);
+    }
+}
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
new file mode 100644
index 0000000..db16998
--- /dev/null
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -0,0 +1,91 @@
+/*
+ * 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.launcher3.widget;
+
+import android.appwidget.AppWidgetHostView;
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.PendingAddItemInfo;
+
+/**
+ * Meta data used for late binding of {@link LauncherAppWidgetProviderInfo}.
+ *
+ * @see {@link PendingAddItemInfo}
+ */
+public class PendingAddWidgetInfo extends PendingAddItemInfo {
+    public int minWidth;
+    public int minHeight;
+    public int minResizeWidth;
+    public int minResizeHeight;
+    public int previewImage;
+    public int icon;
+    public LauncherAppWidgetProviderInfo info;
+    public AppWidgetHostView boundWidget;
+    public Bundle bindOptions = null;
+
+    public PendingAddWidgetInfo(LauncherAppWidgetProviderInfo i, Parcelable data) {
+        if (i.isCustomWidget) {
+            itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
+        } else {
+            itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
+        }
+        this.info = i;
+        componentName = i.provider;
+        minWidth = i.minWidth;
+        minHeight = i.minHeight;
+        minResizeWidth = i.minResizeWidth;
+        minResizeHeight = i.minResizeHeight;
+        previewImage = i.previewImage;
+        icon = i.icon;
+
+        spanX = i.spanX;
+        spanY = i.spanY;
+        minSpanX = i.minSpanX;
+        minSpanY = i.minSpanY;
+    }
+
+    public boolean isCustomWidget() {
+        return itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
+    }
+
+    // Copy constructor
+    public PendingAddWidgetInfo(PendingAddWidgetInfo copy) {
+        minWidth = copy.minWidth;
+        minHeight = copy.minHeight;
+        minResizeWidth = copy.minResizeWidth;
+        minResizeHeight = copy.minResizeHeight;
+        previewImage = copy.previewImage;
+        icon = copy.icon;
+        info = copy.info;
+        boundWidget = copy.boundWidget;
+        componentName = copy.componentName;
+        itemType = copy.itemType;
+        spanX = copy.spanX;
+        spanY = copy.spanY;
+        minSpanX = copy.minSpanX;
+        minSpanY = copy.minSpanY;
+        bindOptions = copy.bindOptions == null ? null : (Bundle) copy.bindOptions.clone();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("PendingAddWidgetInfo package=%s, name=%s",
+                componentName.getPackageName(), componentName.getShortClassName());
+    }
+}
diff --git a/src/com/android/launcher3/PagedViewWidget.java b/src/com/android/launcher3/widget/WidgetCell.java
similarity index 62%
rename from src/com/android/launcher3/PagedViewWidget.java
rename to src/com/android/launcher3/widget/WidgetCell.java
index 107069b..1ae75c3 100644
--- a/src/com/android/launcher3/PagedViewWidget.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010 The Android Open Source Project
+ * 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.
@@ -14,59 +14,78 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.widget;
 
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.TypedValue;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnLayoutChangeListener;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.WidgetPreviewLoader.PreviewLoadRequest;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 
 /**
- * The linear layout used strictly for the widget/wallpaper tab of the customization tray
+ * The linear layout used strictly for the widget tray.
  */
-public class PagedViewWidget extends LinearLayout {
-    static final String TAG = "PagedViewWidgetLayout";
+public class WidgetCell extends LinearLayout implements OnLayoutChangeListener {
 
-    private static boolean sDeletePreviewsWhenDetachedFromWindow = true;
-    private static boolean sRecyclePreviewsWhenDetachedFromWindow = true;
+    private static final String TAG = "WidgetCell";
+    private static final boolean DEBUG = false;
+
+    private static final int FADE_IN_DURATION_MS = 70;
+    private int mPresetPreviewSize;
+
+    private static WidgetCell sShortpressTarget = null;
+
+    private final Rect mOriginalImagePadding = new Rect();
 
     private String mDimensionsFormatString;
-    CheckForShortPress mPendingCheckForShortPress = null;
-    ShortPressListener mShortPressListener = null;
-    boolean mShortPressTriggered = false;
-    static PagedViewWidget sShortpressTarget = null;
-    boolean mIsAppWidget;
-    private final Rect mOriginalImagePadding = new Rect();
+    private CheckForShortPress mPendingCheckForShortPress = null;
+    private ShortPressListener mShortPressListener = null;
+    private boolean mShortPressTriggered = false;
+    private boolean mIsAppWidget;
     private Object mInfo;
-    private WidgetPreviewLoader mWidgetPreviewLoader;
 
-    public PagedViewWidget(Context context) {
+    private WidgetPreviewLoader mWidgetPreviewLoader;
+    private PreviewLoadRequest mActiveRequest;
+
+    public WidgetCell(Context context) {
         this(context, null);
     }
 
-    public PagedViewWidget(Context context, AttributeSet attrs) {
+    public WidgetCell(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
-    public PagedViewWidget(Context context, AttributeSet attrs, int defStyle) {
+    public WidgetCell(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
         final Resources r = context.getResources();
         mDimensionsFormatString = r.getString(R.string.widget_dims_format);
+        mPresetPreviewSize = r.getDimensionPixelSize(R.dimen.widget_preview_size);
 
         setWillNotDraw(false);
         setClipToPadding(false);
         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
+
     }
 
     @Override
@@ -92,29 +111,36 @@
         }
     }
 
-    public static void setDeletePreviewsWhenDetachedFromWindow(boolean value) {
-        sDeletePreviewsWhenDetachedFromWindow = value;
-    }
-
-    public static void setRecyclePreviewsWhenDetachedFromWindow(boolean value) {
-        sRecyclePreviewsWhenDetachedFromWindow = value;
-    }
-
     @Override
     protected void onDetachedFromWindow() {
+        if (DEBUG) {
+            Log.d(TAG, String.format("[tag=%s] onDetachedFromWindow", getTagToString()));
+        }
         super.onDetachedFromWindow();
+        deletePreview(false);
+    }
 
-        if (sDeletePreviewsWhenDetachedFromWindow) {
+    public void reset() {
+        ImageView image = (ImageView) findViewById(R.id.widget_preview);
+        final TextView name = (TextView) findViewById(R.id.widget_name);
+        final TextView dims = (TextView) findViewById(R.id.widget_dims);
+        image.setImageDrawable(null);
+        name.setText(null);
+        dims.setText(null);
+    }
+
+    public void deletePreview(boolean recycleImage) {
+        if (recycleImage) {
             final ImageView image = (ImageView) findViewById(R.id.widget_preview);
             if (image != null) {
-                FastBitmapDrawable preview = (FastBitmapDrawable) image.getDrawable();
-                if (sRecyclePreviewsWhenDetachedFromWindow &&
-                        mInfo != null && preview != null && preview.getBitmap() != null) {
-                    mWidgetPreviewLoader.recycleBitmap(mInfo, preview.getBitmap());
-                }
                 image.setImageDrawable(null);
             }
         }
+
+        if (mActiveRequest != null) {
+            mActiveRequest.cancel(recycleImage);
+            mActiveRequest = null;
+        }
     }
 
     public void applyFromAppWidgetProviderInfo(LauncherAppWidgetProviderInfo info,
@@ -156,14 +182,19 @@
     public int[] getPreviewSize() {
         final ImageView i = (ImageView) findViewById(R.id.widget_preview);
         int[] maxSize = new int[2];
-        maxSize[0] = i.getWidth() - mOriginalImagePadding.left - mOriginalImagePadding.right;
-        maxSize[1] = i.getHeight() - mOriginalImagePadding.top;
+        maxSize[0] = mPresetPreviewSize;
+        maxSize[1] = mPresetPreviewSize;
         return maxSize;
     }
 
-    void applyPreview(FastBitmapDrawable preview, int index) {
-        final PagedViewWidgetImageView image =
-            (PagedViewWidgetImageView) findViewById(R.id.widget_preview);
+    public void applyPreview(Bitmap bitmap) {
+        FastBitmapDrawable preview = new FastBitmapDrawable(bitmap);
+        final WidgetImageView image =
+            (WidgetImageView) findViewById(R.id.widget_preview);
+        if (DEBUG) {
+            Log.d(TAG, String.format("[tag=%s] applyPreview preview: %s",
+                    getTagToString(), preview));
+        }
         if (preview != null) {
             image.mAllowRequestLayout = false;
             image.setImageDrawable(preview);
@@ -176,8 +207,10 @@
                         mOriginalImagePadding.right,
                         mOriginalImagePadding.bottom);
             }
-            image.setAlpha(1f);
+            image.setAlpha(0f);
+            image.animate().alpha(1.0f).setDuration(FADE_IN_DURATION_MS);
             image.mAllowRequestLayout = true;
+            image.requestLayout();
         }
     }
 
@@ -194,8 +227,8 @@
         public void run() {
             if (sShortpressTarget != null) return;
             if (mShortPressListener != null) {
-                mShortPressListener.onShortPress(PagedViewWidget.this);
-                sShortpressTarget = PagedViewWidget.this;
+                mShortPressListener.onShortPress(WidgetCell.this);
+                sShortpressTarget = WidgetCell.this;
             }
             mShortPressTriggered = true;
         }
@@ -222,7 +255,7 @@
         removeShortPressCallback();
         if (mShortPressTriggered) {
             if (mShortPressListener != null) {
-                mShortPressListener.cleanUpShortPress(PagedViewWidget.this);
+                mShortPressListener.cleanUpShortPress(WidgetCell.this);
             }
             mShortPressTriggered = false;
         }
@@ -259,4 +292,54 @@
         // we just always mark the touch event as handled.
         return true;
     }
+
+    public void ensurePreview() {
+        if (mActiveRequest != null) {
+            return;
+        }
+        int[] size = getPreviewSize();
+        if (DEBUG) {
+            Log.d(TAG, String.format("[tag=%s] ensurePreview (%d, %d):",
+                    getTagToString(), size[0], size[1]));
+        }
+
+        if (size[0] <= 0 || size[1] <= 0) {
+            addOnLayoutChangeListener(this);
+            return;
+        }
+        Bitmap[] immediateResult = new Bitmap[1];
+        mActiveRequest = mWidgetPreviewLoader.getPreview(mInfo, size[0], size[1], this,
+                immediateResult);
+        if (immediateResult[0] != null) {
+            applyPreview(immediateResult[0]);
+        }
+    }
+
+    @Override
+    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+            int oldTop, int oldRight, int oldBottom) {
+        removeOnLayoutChangeListener(this);
+        ensurePreview();
+    }
+
+    public int getActualItemWidth() {
+        ItemInfo info = (ItemInfo) getTag();
+        int[] size = getPreviewSize();
+        int cellWidth = LauncherAppState.getInstance()
+                .getDynamicGrid().getDeviceProfile().cellWidthPx;
+
+        return Math.min(size[0], info.spanX * cellWidth);
+    }
+
+    /**
+     * Helper method to get the string info of the tag.
+     */
+    private String getTagToString() {
+        if (getTag() instanceof PendingAddWidgetInfo) {
+            return ((PendingAddWidgetInfo)getTag()).toString();
+        } else if (getTag() instanceof PendingAddShortcutInfo) {
+            return ((PendingAddShortcutInfo)getTag()).toString();
+        }
+        return "";
+    }
 }
diff --git a/src/com/android/launcher3/PagedViewWidgetImageView.java b/src/com/android/launcher3/widget/WidgetImageView.java
similarity index 88%
rename from src/com/android/launcher3/PagedViewWidgetImageView.java
rename to src/com/android/launcher3/widget/WidgetImageView.java
index 7d82795..75167bc 100644
--- a/src/com/android/launcher3/PagedViewWidgetImageView.java
+++ b/src/com/android/launcher3/widget/WidgetImageView.java
@@ -14,17 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.widget;
 
 import android.content.Context;
 import android.graphics.Canvas;
 import android.util.AttributeSet;
 import android.widget.ImageView;
 
-public class PagedViewWidgetImageView extends ImageView {
+public class WidgetImageView extends ImageView {
     public boolean mAllowRequestLayout = true;
 
-    public PagedViewWidgetImageView(Context context, AttributeSet attrs) {
+    public WidgetImageView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
@@ -44,6 +44,5 @@
 
         super.onDraw(canvas);
         canvas.restore();
-
     }
 }
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
new file mode 100644
index 0000000..292a5de
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -0,0 +1,372 @@
+/*
+ * 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.launcher3.widget;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeleteDropTarget;
+import com.android.launcher3.DragController;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget.DragObject;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.Folder;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.PendingAddItemInfo;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.Workspace;
+
+import java.util.ArrayList;
+
+/**
+ * The widgets list view container.
+ */
+public class WidgetsContainerView extends FrameLayout implements Insettable, View.OnTouchListener,
+        View.OnLongClickListener, DragSource{
+
+    private static final String TAG = "WidgetContainerView";
+    private static final boolean DEBUG = false;
+
+    /* {@link RecyclerView} will keep following # of views in cache, before recycling. */
+    private static final int WIDGET_CACHE_SIZE = 2;
+
+    /* Global instances that are used inside this container. */
+    private Launcher mLauncher;
+    private DragController mDragController;
+    private IconCache mIconCache;
+
+    /* Data model for the widget */
+    private WidgetsModel mWidgets;
+
+    /* Recycler view related member variables */
+    private RecyclerView mView;
+    private WidgetsListAdapter mAdapter;
+
+    /* Dragging related. */
+    private boolean mDraggingWidget = false;    // TODO(hyunyoungs): seems not needed? check!
+    private Point mLastTouchDownPos = new Point();
+
+    /* Rendering related. */
+    private WidgetPreviewLoader mWidgetPreviewLoader;
+    private Rect mPadding = new Rect();
+
+    public WidgetsContainerView(Context context) {
+        this(context, null);
+    }
+
+    public WidgetsContainerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public WidgetsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = (Launcher) context;
+        mDragController = mLauncher.getDragController();
+
+        mAdapter = new WidgetsListAdapter(context, this, mLauncher, this, mLauncher);
+        mWidgets = new WidgetsModel(context, mAdapter);
+        mAdapter.setWidgetsModel(mWidgets);
+        mIconCache = (LauncherAppState.getInstance()).getIconCache();
+
+        if (DEBUG) {
+            Log.d(TAG, "WidgetsContainerView constructor");
+        }
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        if (DEBUG) {
+            Log.d(TAG, String.format("onFinishInflate [widgets size=%d]",
+                    mWidgets.getPackageSize()));
+        }
+        mView = (RecyclerView) findViewById(R.id.widgets_list_view);
+        mView.setAdapter(mAdapter);
+        mView.setLayoutManager(new LinearLayoutManager(getContext()));
+        mView.setItemViewCacheSize(WIDGET_CACHE_SIZE);
+
+        mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+                getPaddingBottom());
+    }
+
+    //
+    // Returns views used for launcher transitions.
+    //
+
+    public View getContentView() {
+        return findViewById(R.id.widgets_content);
+    }
+
+    public View getRevealView() {
+        // TODO(hyunyoungs): temporarily use apps view transition.
+        return findViewById(R.id.widgets_reveal_view);
+    }
+
+    public void scrollToTop() {
+        mView.scrollToPosition(0);
+        if (DEBUG) {
+            Log.d(TAG, String.format("scrollToTop, [widgets size=%d]",
+                    mWidgets.getPackageSize()));
+        }
+    }
+
+    //
+    // Touch related handling.
+    //
+
+    @Override
+    public boolean onLongClick(View v) {
+        if (DEBUG) {
+            Log.d(TAG, String.format("onLonglick [v=%s]", v));
+        }
+
+        // Return early if this is not initiated from a touch
+        if (!v.isInTouchMode()) return false;
+        // When we have exited all apps or are in transition, disregard long clicks
+        if (!mLauncher.isWidgetsViewVisible() ||
+                mLauncher.getWorkspace().isSwitchingState()) return false;
+        // Return if global dragging is not enabled
+        Log.d(TAG, String.format("onLonglick dragging enabled?.", v));
+        if (!mLauncher.isDraggingEnabled()) return false;
+
+        return beginDragging(v);
+    }
+
+    private boolean beginDragging(View v) {
+        if (v instanceof WidgetCell) {
+            if (!beginDraggingWidget((WidgetCell) v)) {
+                return false;
+            }
+        } else {
+            Log.e(TAG, "Unexpected dragging view: " + v);
+        }
+
+        // We delay entering spring-loaded mode slightly to make sure the UI
+        // thready is free of any work.
+        postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                // We don't enter spring-loaded mode if the drag has been cancelled
+                if (mLauncher.getDragController().isDragging()) {
+                    // Go into spring loaded mode (must happen before we startDrag())
+                    mLauncher.enterSpringLoadedDragMode();
+                }
+            }
+        }, 150);
+
+        return true;
+    }
+
+    private boolean beginDraggingWidget(WidgetCell v) {
+        mDraggingWidget = true;
+        // Get the widget preview as the drag representation
+        ImageView image = (ImageView) v.findViewById(R.id.widget_preview);
+        PendingAddItemInfo createItemInfo = (PendingAddItemInfo) v.getTag();
+
+        // If the ImageView doesn't have a drawable yet, the widget preview hasn't been loaded and
+        // we abort the drag.
+        if (image.getDrawable() == null) {
+            mDraggingWidget = false;
+            return false;
+        }
+
+        // Compose the drag image
+        Bitmap preview;
+        Bitmap outline;
+        float scale = 1f;
+        Point previewPadding = null;
+
+        if (createItemInfo instanceof PendingAddWidgetInfo) {
+            // This can happen in some weird cases involving multi-touch. We can't start dragging
+            // the widget if this is null, so we break out.
+
+            PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) createItemInfo;
+            int[] size = mLauncher.getWorkspace().estimateItemSize(createWidgetInfo, true);
+
+            FastBitmapDrawable previewDrawable = (FastBitmapDrawable) image.getDrawable();
+            float minScale = 1.25f;
+            int maxWidth = Math.min((int) (previewDrawable.getIntrinsicWidth() * minScale), size[0]);
+
+            int[] previewSizeBeforeScale = new int[1];
+            preview = getWidgetPreviewLoader().generateWidgetPreview(createWidgetInfo.info,
+                    maxWidth, null, previewSizeBeforeScale);
+            // Compare the size of the drag preview to the preview in the AppsCustomize tray
+            int previewWidthInAppsCustomize = Math.min(previewSizeBeforeScale[0],
+                    v.getActualItemWidth());
+            scale = previewWidthInAppsCustomize / (float) preview.getWidth();
+
+            // The bitmap in the AppsCustomize tray is always the the same size, so there
+            // might be extra pixels around the preview itself - this accounts for that
+            if (previewWidthInAppsCustomize < previewDrawable.getIntrinsicWidth()) {
+                int padding =
+                        (previewDrawable.getIntrinsicWidth() - previewWidthInAppsCustomize) / 2;
+                previewPadding = new Point(padding, 0);
+            }
+        } else {
+            PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) v.getTag();
+            Drawable icon = mIconCache.getFullResIcon(createShortcutInfo.activityInfo);
+            preview = Utilities.createIconBitmap(icon, mLauncher);
+            createItemInfo.spanX = createItemInfo.spanY = 1;
+        }
+
+        // Don't clip alpha values for the drag outline if we're using the default widget preview
+        boolean clipAlpha = !(createItemInfo instanceof PendingAddWidgetInfo &&
+                (((PendingAddWidgetInfo) createItemInfo).previewImage == 0));
+
+        // Save the preview for the outline generation, then dim the preview
+        outline = Bitmap.createScaledBitmap(preview, preview.getWidth(), preview.getHeight(),
+                false);
+
+        // Start the drag
+        mLauncher.lockScreenOrientation();
+        mLauncher.getWorkspace().onDragStartedWithItem(createItemInfo, outline, clipAlpha);
+        mDragController.startDrag(image, preview, this, createItemInfo,
+                DragController.DRAG_ACTION_COPY, previewPadding, scale);
+        outline.recycle();
+        preview.recycle();
+        return true;
+    }
+
+    /*
+     * @see android.view.View.OnTouchListener#onTouch(android.view.View, android.view.MotionEvent)
+     */
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        Log.d(TAG, String.format("onTouch [MotionEvent=%s]", ev));
+        if (ev.getAction() == MotionEvent.ACTION_DOWN ||
+                ev.getAction() == MotionEvent.ACTION_MOVE) {
+            mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY());
+        }
+        return false;
+    }
+
+    //
+    // Drag related handling methods that implement {@link DragSource} interface.
+    //
+
+    @Override
+    public boolean supportsFlingToDelete() {
+        return false;
+    }
+
+    @Override
+    public boolean supportsAppInfoDropTarget() {
+        return true;
+    }
+
+    /*
+     * Both this method and {@link #supportsFlingToDelete} has to return {@code false} for the
+     * {@link DeleteDropTarget} to be invisible.)
+     */
+    @Override
+    public boolean supportsDeleteDropTarget() {
+        return false;
+    }
+
+    @Override
+    public float getIntrinsicIconScaleFactor() {
+        return 0;
+    }
+
+    @Override
+    public void onFlingToDeleteCompleted() {
+        // We just dismiss the drag when we fling, so cleanup here
+        mLauncher.exitSpringLoadedDragModeDelayed(true,
+                Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+        mLauncher.unlockScreenOrientation(false);
+    }
+
+    @Override
+    public void onDropCompleted(View target, DragObject d, boolean isFlingToDelete,
+            boolean success) {
+        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
+                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
+            // Exit spring loaded mode if we have not successfully dropped or have not handled the
+            // drop in Workspace
+            mLauncher.exitSpringLoadedDragModeDelayed(true,
+                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
+        }
+        mLauncher.unlockScreenOrientation(false);
+
+        // Display an error message if the drag failed due to there not being enough space on the
+        // target layout we were dropping on.
+        if (!success) {
+            boolean showOutOfSpaceMessage = false;
+            if (target instanceof Workspace) {
+                int currentScreen = mLauncher.getCurrentWorkspaceScreen();
+                Workspace workspace = (Workspace) target;
+                CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
+                ItemInfo itemInfo = (ItemInfo) d.dragInfo;
+                if (layout != null) {
+                    layout.calculateSpans(itemInfo);
+                    showOutOfSpaceMessage =
+                            !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
+                }
+            }
+            if (showOutOfSpaceMessage) {
+                mLauncher.showOutOfSpaceMessage(false);
+            }
+            d.deferDragViewCleanupPostAnimation = false;
+        }
+    }
+
+    //
+    // Container rendering related.
+    //
+
+    /*
+     * @see Insettable#setInsets(Rect)
+     */
+    @Override
+    public void setInsets(Rect insets) {
+        setPadding(mPadding.left + insets.left, mPadding.top + insets.top,
+                mPadding.right + insets.right, mPadding.bottom + insets.bottom);
+    }
+
+    /**
+     * Initialize the widget data model.
+     */
+    public void addWidgets(ArrayList<Object> widgetsShortcuts, PackageManager pm) {
+        mWidgets.addWidgetsAndShortcuts(widgetsShortcuts, pm);
+    }
+
+    private WidgetPreviewLoader getWidgetPreviewLoader() {
+        if (mWidgetPreviewLoader == null) {
+            mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache();
+        }
+        return mWidgetPreviewLoader;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java
new file mode 100644
index 0000000..afeb2d3
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java
@@ -0,0 +1,177 @@
+/*
+ * 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.launcher3.widget;
+
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.launcher3.IconCache;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.R;
+import com.android.launcher3.WidgetPreviewLoader;
+import com.android.launcher3.compat.UserHandleCompat;
+
+import java.util.List;
+
+/**
+ * List view adapter for the widget tray.
+ *
+ * <p>Memory vs. Performance:
+ * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling
+ * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is
+ * only a single type of view.
+ */
+public class WidgetsListAdapter extends Adapter<WidgetsRowViewHolder> {
+
+    private static final String TAG = "WidgetsListAdapter";
+    private static final boolean DEBUG = false;
+
+    private Context mContext;
+    private Launcher mLauncher;
+    private LayoutInflater mLayoutInflater;
+    private IconCache mIconCache;
+
+    private WidgetsModel mWidgetsModel;
+    private WidgetPreviewLoader mWidgetPreviewLoader;
+
+    private View.OnTouchListener mTouchListener;
+    private View.OnClickListener mIconClickListener;
+    private View.OnLongClickListener mIconLongClickListener;
+
+
+    public WidgetsListAdapter(Context context,
+            View.OnTouchListener touchListener,
+            View.OnClickListener iconClickListener,
+            View.OnLongClickListener iconLongClickListener,
+            Launcher launcher) {
+        mLayoutInflater = LayoutInflater.from(context);
+        mContext = context;
+
+        mTouchListener = touchListener;
+        mIconClickListener = iconClickListener;
+        mIconLongClickListener = iconLongClickListener;
+
+        mLauncher = launcher;
+        mIconCache = LauncherAppState.getInstance().getIconCache();
+    }
+
+    public void setWidgetsModel(WidgetsModel w) {
+        mWidgetsModel = w;
+    }
+
+    @Override
+    public int getItemCount() {
+        return mWidgetsModel.getPackageSize();
+    }
+
+    @Override
+    public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) {
+        List<Object> infoList = mWidgetsModel.getSortedWidgets(pos);
+
+        ViewGroup row = ((ViewGroup) holder.getContent().findViewById(R.id.widgets_cell_list));
+        if (DEBUG) {
+            Log.d(TAG, String.format(
+                    "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]",
+                    pos, infoList.size(), row.getChildCount()));
+        }
+
+        // Add more views.
+        // if there are too many, hide them.
+        int diff = infoList.size() - row.getChildCount();
+        if (diff > 0) {
+            for (int i = 0; i < diff; i++) {
+                WidgetCell widget = new WidgetCell(mContext);
+                widget = (WidgetCell) mLayoutInflater.inflate(
+                        R.layout.widget_cell, row, false);
+
+                // set up touch.
+                widget.setOnClickListener(mIconClickListener);
+                widget.setOnLongClickListener(mIconLongClickListener);
+                widget.setOnTouchListener(mTouchListener);
+                row.addView(widget);
+            }
+        } else if (diff < 0) {
+            for (int i=infoList.size() ; i < row.getChildCount(); i++) {
+                row.getChildAt(i).setVisibility(View.GONE);
+            }
+        }
+
+        // Bind the views in the application info section.
+        PackageItemInfo infoOut = mWidgetsModel.getPackageItemInfo(pos);
+        if (infoOut.usingLowResIcon) {
+            // TODO(hyunyoungs): call this in none UI thread in the same way as BubbleTextView.
+            mIconCache.getTitleAndIconForApp(infoOut.packageName,
+                    UserHandleCompat.myUserHandle(), false /* useLowResIcon */, infoOut);
+        }
+        ((TextView) holder.getContent().findViewById(R.id.section)).setText(infoOut.title);
+        ImageView iv = (ImageView) holder.getContent().findViewById(R.id.section_image);
+        iv.setImageBitmap(infoOut.iconBitmap);
+
+        // Bind the view in the widget horizontal tray region.
+        for (int i=0; i < infoList.size(); i++) {
+            WidgetCell widget = (WidgetCell) row.getChildAt(i);
+            widget.reset();
+            if (getWidgetPreviewLoader() == null) {
+                return;
+            }
+            if (infoList.get(i) instanceof LauncherAppWidgetProviderInfo) {
+                LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) infoList.get(i);
+                PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(info, null);
+                widget.setTag(pawi);
+                widget.applyFromAppWidgetProviderInfo(info, -1, mWidgetPreviewLoader);
+            } else if (infoList.get(i) instanceof ResolveInfo) {
+                ResolveInfo info = (ResolveInfo) infoList.get(i);
+                PendingAddShortcutInfo pasi = new PendingAddShortcutInfo(info.activityInfo);
+                widget.setTag(pasi);
+                widget.applyFromResolveInfo(mLauncher.getPackageManager(), info, mWidgetPreviewLoader);
+            }
+            widget.setVisibility(View.VISIBLE);
+            widget.ensurePreview();
+        }
+    }
+
+    @Override
+    public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        if (DEBUG) {
+            Log.v(TAG, String.format("\nonCreateViewHolder, [widget#=%d]", viewType));
+        }
+
+        ViewGroup container = (ViewGroup) mLayoutInflater.inflate(
+                R.layout.widgets_list_row_view, parent, false);
+        return new WidgetsRowViewHolder(container);
+    }
+
+    @Override
+    public long getItemId(int pos) {
+        return pos;
+    }
+
+    private WidgetPreviewLoader getWidgetPreviewLoader() {
+        if (mWidgetPreviewLoader == null) {
+            mWidgetPreviewLoader = LauncherAppState.getInstance().getWidgetCache();
+        }
+        return mWidgetPreviewLoader;
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsModel.java b/src/com/android/launcher3/widget/WidgetsModel.java
new file mode 100644
index 0000000..71a7b94
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetsModel.java
@@ -0,0 +1,128 @@
+
+package com.android.launcher3.widget;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+
+import com.android.launcher3.IconCache;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LauncherAppWidgetProviderInfo;
+import com.android.launcher3.LauncherModel.WidgetAndShortcutNameComparator;
+import com.android.launcher3.compat.UserHandleCompat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Widgets data model that is used by the adapters of the widget views and controllers.
+ *
+ * <p> The widgets and shortcuts are organized using package name as its index.
+ */
+public class WidgetsModel {
+
+    private static final String TAG = "WidgetsModel";
+    private static final boolean DEBUG = false;
+
+    /* List of packages that is tracked by this model. */
+    private List<PackageItemInfo> mPackageItemInfos = new ArrayList<>();
+
+    /* Map of widgets and shortcuts that are tracked per package. */
+    private Map<PackageItemInfo, ArrayList<Object>> mWidgetsList = new HashMap<>();
+
+    /* Notifies the adapter when data changes. */
+    private RecyclerView.Adapter mAdapter;
+
+    private Comparator mWidgetAndShortcutNameComparator;
+
+    private IconCache mIconCache;
+
+    public WidgetsModel(Context context, RecyclerView.Adapter adapter) {
+        mAdapter = adapter;
+        mWidgetAndShortcutNameComparator = new WidgetAndShortcutNameComparator(context);
+        mIconCache = LauncherAppState.getInstance().getIconCache();
+    }
+
+    // Access methods that may be deleted if the private fields are made package-private.
+    public int getPackageSize() {
+        return mPackageItemInfos.size();
+    }
+
+    // Access methods that may be deleted if the private fields are made package-private.
+    public PackageItemInfo getPackageItemInfo(int pos) {
+        return mPackageItemInfos.get(pos);
+    }
+
+    public List<Object> getSortedWidgets(int pos) {
+        return mWidgetsList.get(mPackageItemInfos.get(pos));
+    }
+
+    public void addWidgetsAndShortcuts(ArrayList<Object> widgetsShortcuts, PackageManager pm) {
+        if (DEBUG) {
+            Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + widgetsShortcuts.size());
+        }
+
+        // Temporary list for {@link PackageItemInfos} to avoid having to go through
+        // {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
+        HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
+
+        // clear the lists.
+        mWidgetsList.clear();
+        mPackageItemInfos.clear();
+
+        // add and update.
+        for (Object o: widgetsShortcuts) {
+            String packageName = "";
+            if (o instanceof LauncherAppWidgetProviderInfo) {
+                LauncherAppWidgetProviderInfo widgetInfo = (LauncherAppWidgetProviderInfo) o;
+                packageName = widgetInfo.provider.getPackageName();
+            } else if (o instanceof ResolveInfo) {
+                ResolveInfo resolveInfo = (ResolveInfo) o;
+                packageName = resolveInfo.activityInfo.packageName;
+            } else {
+                Log.e(TAG, String.format("addWidgetsAndShortcuts, nothing added for class=%s",
+                        o.getClass().toString()));
+            }
+
+            PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName);
+            ArrayList<Object> widgetsShortcutsList = mWidgetsList.get(pInfo);
+            if (widgetsShortcutsList != null) {
+                widgetsShortcutsList.add(o);
+            } else {
+                widgetsShortcutsList = new ArrayList<Object>();
+                widgetsShortcutsList.add(o);
+
+                pInfo = new PackageItemInfo(packageName);
+                mIconCache.getTitleAndIconForApp(packageName, UserHandleCompat.myUserHandle(),
+                        true /* useLowResIcon */, pInfo);
+                mWidgetsList.put(pInfo, widgetsShortcutsList);
+                tmpPackageItemInfos.put(packageName,  pInfo);
+                mPackageItemInfos.add(pInfo);
+            }
+        }
+
+        // sort.
+        sortPackageItemInfos();
+        for (PackageItemInfo p: mPackageItemInfos) {
+            Collections.sort(mWidgetsList.get(p), mWidgetAndShortcutNameComparator);
+        }
+
+        // notify.
+        mAdapter.notifyDataSetChanged();
+    }
+
+    private void sortPackageItemInfos() {
+        Collections.sort(mPackageItemInfos, new Comparator<PackageItemInfo>() {
+            @Override
+            public int compare(PackageItemInfo lhs, PackageItemInfo rhs) {
+                return lhs.title.toString().compareTo(rhs.title.toString());
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/widget/WidgetsRowView.java b/src/com/android/launcher3/widget/WidgetsRowView.java
new file mode 100644
index 0000000..5466738
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetsRowView.java
@@ -0,0 +1,90 @@
+/*
+ * 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.launcher3.widget;
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.TextView;
+
+import com.android.launcher3.R;
+
+/**
+ * Layout used for widget tray rows for each app. For performance, this view can be replaced with
+ * a {@link RecyclerView} in the future if we settle on scrollable single row for the widgets.
+ * If we decide on collapsable grid, then HorizontalScrollView can be replaced with a
+ * {@link GridLayout}.
+ */
+public class WidgetsRowView extends HorizontalScrollView {
+    static final String TAG = "WidgetsRow";
+
+    private Runnable mOnLayoutListener;
+    private String mAppName;
+
+    public WidgetsRowView(Context context, String appName) {
+        super(context, null, 0);
+        mAppName = appName;
+    }
+
+    /**
+     * Clears all the key listeners for the individual widgets.
+     */
+    public void resetChildrenOnKeyListeners() {
+        int childCount = getChildCount();
+        for (int j = 0; j < childCount; ++j) {
+            getChildAt(j).setOnKeyListener(null);
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        TextView tv = (TextView) findViewById(R.id.widget_name);
+        tv.setText(mAppName);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mOnLayoutListener = null;
+    }
+
+    public void setOnLayoutListener(Runnable r) {
+        mOnLayoutListener = r;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (mOnLayoutListener != null) {
+            mOnLayoutListener.run();
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean result = super.onTouchEvent(event);
+        return result;
+    }
+
+    public static class LayoutParams extends FrameLayout.LayoutParams {
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
new file mode 100644
index 0000000..99a192c
--- /dev/null
+++ b/src/com/android/launcher3/widget/WidgetsRowViewHolder.java
@@ -0,0 +1,36 @@
+/*
+ * 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.launcher3.widget;
+
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class WidgetsRowViewHolder extends ViewHolder {
+
+    ViewGroup mContent;
+
+   public WidgetsRowViewHolder(ViewGroup v) {
+        super(v);
+        mContent = v;
+    }
+
+    ViewGroup getContent() {
+        return mContent;
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
index 762a52b..eba4ade 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -1,4 +1,4 @@
-# Copyright (C) 2011 The Android Open Source Project
+# 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.
@@ -12,5 +12,26 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-#LOCAL_PATH := $(call my-dir)
-#include $(call all-makefiles-under,$(LOCAL_PATH))
+
+LOCAL_PATH := $(call my-dir)
+
+src_dirs := src
+res_dirs := res
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
+LOCAL_AAPT_FLAGS := --auto-add-overlay
+
+LOCAL_SDK_VERSION := 21
+
+LOCAL_PACKAGE_NAME := Launcher3Tests
+
+LOCAL_INSTRUMENTATION_FOR := Launcher3
+
+include $(BUILD_PACKAGE)
diff --git a/tests/stress/AndroidManifest.xml b/tests/AndroidManifest.xml
similarity index 83%
rename from tests/stress/AndroidManifest.xml
rename to tests/AndroidManifest.xml
index bcca1ff..42ae5a3 100644
--- a/tests/stress/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -1,5 +1,5 @@
 <?xml version="2.0" encoding="utf-8"?>
-<!-- Copyright (C) 2010 The Android Open Source Project
+<!-- 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.
@@ -15,7 +15,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.launcher3.stress.launcherrotation">
+    package="com.android.launcher3.tests">
 
     <application>
         <uses-library android:name="android.test.runner" />
@@ -24,6 +24,6 @@
     <instrumentation
         android:name="android.test.InstrumentationTestRunner"
         android:targetPackage="com.android.launcher3"
-        android:label="Rotation stress test using Launcher2">
+        android:label="Unit tests for Launcher3">
     </instrumentation>
 </manifest>
diff --git a/tests/res/values/string.xml b/tests/res/values/string.xml
new file mode 100644
index 0000000..3c1ec5c
--- /dev/null
+++ b/tests/res/values/string.xml
@@ -0,0 +1,21 @@
+<!-- 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <!-- Dummy string for tests. [DO NOT TRANSLATE] -->
+    <string name="dummy" >Dummy string for tests.</string>
+
+</resources>
diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java
new file mode 100644
index 0000000..ea87014
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.launcher3;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.KeyEvent;
+
+import com.android.launcher3.util.FocusLogic;
+
+/**
+ * Tests the {@link FocusLogic} class that handles key event based focus handling.
+ */
+@SmallTest
+public final class FocusLogicTest extends AndroidTestCase {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Nothing to set up as this class only tests static methods.
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Nothing to tear down as this class only tests static methods.
+    }
+
+    public void testShouldConsume() {
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_LEFT));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_RIGHT));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_UP));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DPAD_DOWN));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_HOME));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_MOVE_END));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_UP));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_PAGE_DOWN));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_DEL));
+         assertTrue(FocusLogic.shouldConsume(KeyEvent.KEYCODE_FORWARD_DEL));
+    }
+
+    public void testCreateSparseMatrix() {
+         // Either, 1) create a helper method to generate/instantiate all possible cell layout that
+         // may get created in real world to test this method. OR 2) Move all the matrix
+         // management routine to celllayout and write tests for them.
+    }
+}
diff --git a/tests/stress/Android.mk b/tests/stress/Android.mk
deleted file mode 100644
index 68289bd..0000000
--- a/tests/stress/Android.mk
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) 2011 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.
-#
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-# We only want this apk build for tests.
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_JAVA_LIBRARIES := android.test.runner
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := LauncherRotationStressTest
-
-LOCAL_CERTIFICATE := shared
-
-LOCAL_INSTRUMENTATION_FOR := Launcher2
-
-include $(BUILD_PACKAGE)
diff --git a/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java b/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java
deleted file mode 100644
index a5b85eb..0000000
--- a/tests/stress/src/com/android/launcher3/stress/LauncherRotationStressTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2011 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.stress;
-
-
-import com.android.launcher3.Launcher;
-
-import android.content.pm.ActivityInfo;
-import android.os.SystemClock;
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.RepetitiveTest;
-import android.util.Log;
-
-/**
- * Run rotation stress test using Launcher2 for 50 iterations.
- */
-public class LauncherRotationStressTest extends ActivityInstrumentationTestCase2<Launcher> {
-
-    private static final int NUM_ITERATIONS = 50;
-    private static final int WAIT_TIME_MS = 500;
-    private static final String LOG_TAG = "LauncherRotationStressTest";
-
-    public LauncherRotationStressTest() {
-        super(Launcher.class);
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-    }
-
-    @Override
-    protected void tearDown() throws Exception {
-        super.tearDown();
-    }
-
-    @RepetitiveTest(numIterations=NUM_ITERATIONS)
-    public void testLauncherRotationStress() throws Exception {
-        Launcher launcher = getActivity();
-        getInstrumentation().waitForIdleSync();
-        SystemClock.sleep(WAIT_TIME_MS);
-        launcher.setRequestedOrientation(
-                ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
-        getInstrumentation().waitForIdleSync();
-        SystemClock.sleep(WAIT_TIME_MS);
-        launcher.setRequestedOrientation(
-                ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
-    }
-}