Merge "Drags originating in Folder exit spring loaded mode when completed." into ub-launcher3-master
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9014234..1f908d6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -90,39 +90,6 @@
             </intent-filter>
         </activity>
 
-        <!-- ENABLE_FOR_TESTING
-
-        <activity
-            android:name="com.android.launcher3.testing.LauncherExtension"
-            android:launchMode="singleTask"
-            android:clearTaskOnLaunch="true"
-            android:stateNotNeeded="true"
-            android:theme="@style/Theme"
-            android:windowSoftInputMode="adjustPan"
-            android:screenOrientation="nosensor"
-            android:enabled="false">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.HOME" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.MONKEY"/>
-            </intent-filter>
-        </activity>
-
-        -->
-
-        <activity
-            android:name="com.android.launcher3.ToggleWeightWatcher"
-            android:label="@string/toggle_weight_watcher"
-            android:enabled="@bool/debug_memory_enabled"
-            android:icon="@mipmap/ic_launcher_home">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-
         <activity
             android:name="com.android.launcher3.WallpaperPickerActivity"
             android:theme="@style/Theme.WallpaperPicker"
@@ -157,27 +124,6 @@
             android:process=":settings_process">
         </activity>
 
-        <!-- Debugging tools -->
-        <activity
-            android:name="com.android.launcher3.MemoryDumpActivity"
-            android:theme="@android:style/Theme.NoDisplay"
-            android:label="@string/debug_memory_activity"
-            android:enabled="@bool/debug_memory_enabled"
-            android:excludeFromRecents="true"
-            android:icon="@mipmap/ic_launcher_home"
-            >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-
-        <service android:name="com.android.launcher3.MemoryTracker"
-            android:enabled="@bool/debug_memory_enabled"
-            >
-        </service>
-
         <!-- Intent received used to install shortcuts from other applications -->
         <receiver
             android:name="com.android.launcher3.InstallShortcutReceiver"
@@ -204,5 +150,54 @@
 
         <meta-data android:name="android.nfc.disable_beam_default"
                        android:value="true" />
+
+        <!-- ENABLE_FOR_TESTING
+
+        <activity
+            android:name="com.android.launcher3.testing.LauncherExtension"
+            android:launchMode="singleTask"
+            android:clearTaskOnLaunch="true"
+            android:stateNotNeeded="true"
+            android:theme="@style/Theme"
+            android:windowSoftInputMode="adjustPan"
+            android:screenOrientation="nosensor"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.HOME" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.MONKEY"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name="com.android.launcher3.testing.MemoryDumpActivity"
+            android:theme="@android:style/Theme.NoDisplay"
+            android:label="* HPROF"
+            android:excludeFromRecents="true"
+            android:icon="@mipmap/ic_launcher_home"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name="com.android.launcher3.testing.ToggleWeightWatcher"
+            android:label="Show Mem"
+            android:icon="@mipmap/ic_launcher_home">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <service android:name="com.android.launcher3.testing.MemoryTracker" />
+
+        -->
+
     </application>
 </manifest>
diff --git a/WallpaperPicker/res/values/strings.xml b/WallpaperPicker/res/values/strings.xml
index 72b1e15..2bfd476 100644
--- a/WallpaperPicker/res/values/strings.xml
+++ b/WallpaperPicker/res/values/strings.xml
@@ -28,6 +28,9 @@
          usually see this when using another app and trying to set
          an image as the wallpaper -->
     <string name="wallpaper_load_fail">Couldn\'t load image as wallpaper</string>
+    <!-- Error message when an image is selected as a wallpaper,
+         but something goes wrong when the user clicks "Set wallpaper" -->
+    <string name="wallpaper_set_fail">Couldn\'t set image as wallpaper</string>
     <!-- Shown when wallpapers are selected in Wallpaper picker -->
     <!-- String indicating how many media item(s) is(are) selected
             eg. 1 selected [CHAR LIMIT=30] -->
diff --git a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
index 1d8e37d..212fb84 100644
--- a/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
+++ b/WallpaperPicker/src/com/android/gallery3d/common/BitmapCropTask.java
@@ -31,6 +31,9 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.util.Log;
+import android.widget.Toast;
+
+import com.android.launcher3.R;
 
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
@@ -293,22 +296,16 @@
                         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;
+                        // Adjust the left and right values.
+                        roundedTrueCrop.offset(-(roundedTrueCrop.right - fullSize.getWidth()), 0);
                     }
                     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;
+                        // Adjust the top and bottom values.
+                        roundedTrueCrop.offset(0, -(roundedTrueCrop.bottom - fullSize.getHeight()));
                     }
 
                     crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
@@ -401,9 +398,12 @@
     }
 
     @Override
-    protected void onPostExecute(Boolean result) {
+    protected void onPostExecute(Boolean cropSucceeded) {
+        if (!cropSucceeded) {
+            Toast.makeText(mContext, R.string.wallpaper_set_fail, Toast.LENGTH_SHORT).show();
+        }
         if (mOnEndCropHandler != null) {
-            mOnEndCropHandler.run(result);
+            mOnEndCropHandler.run(cropSucceeded);
         }
     }
 }
\ No newline at end of file
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
index b562fbf..4be6f17 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperCropActivity.java
@@ -341,6 +341,9 @@
     public void cropImageAndSetWallpaper(Uri uri,
             BitmapCropTask.OnBitmapCroppedHandler onBitmapCroppedHandler,
             boolean shouldFadeOutOnFinish) {
+        // Give some feedback so user knows something is happening.
+        mProgressView.setVisibility(View.VISIBLE);
+
         boolean centerCrop = getResources().getBoolean(R.bool.center_crop);
         // Get the crop
         boolean ltr = mCropView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
diff --git a/proguard.flags b/proguard.flags
index 22ffa3c..05963f7 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -19,11 +19,6 @@
   public float getAlpha();
 }
 
--keep class com.android.launcher3.BubbleTextView {
-  public void setFastScrollFocus(float);
-  public float getFastScrollFocus();
-}
-
 -keep class com.android.launcher3.ButtonDropTarget {
   public int getTextColor();
 }
@@ -56,8 +51,10 @@
 }
 
 -keep class com.android.launcher3.FastBitmapDrawable {
-  public int getBrightness();
-  public void setBrightness(int);
+  public void setDesaturation(float);
+  public float getDesaturation();
+  public void setBrightness(float);
+  public float getBrightness();
 }
 
 -keep class com.android.launcher3.MemoryDumpActivity {
diff --git a/res/layout/all_apps_search_market.xml b/res/layout/all_apps_search_market.xml
index 1ed5088..a2bb2e7 100644
--- a/res/layout/all_apps_search_market.xml
+++ b/res/layout/all_apps_search_market.xml
@@ -25,5 +25,5 @@
     android:textSize="14sp"
     android:textColor="@color/launcher_accent_color"
     android:textAllCaps="true"
-    android:focusable="false"
+    android:focusable="true"
     android:background="@drawable/all_apps_search_market_bg" />
diff --git a/res/layout/overview_panel.xml b/res/layout/overview_panel.xml
index 1f02dce..4f54f1d 100644
--- a/res/layout/overview_panel.xml
+++ b/res/layout/overview_panel.xml
@@ -33,7 +33,8 @@
         android:text="@string/wallpaper_button_text"
         android:textAllCaps="true"
         android:textColor="@android:color/white"
-        android:textSize="12sp" />
+        android:textSize="12sp"
+        android:focusable="true" />
 
     <TextView
         android:id="@+id/widget_button"
@@ -47,7 +48,8 @@
         android:text="@string/widget_button_text"
         android:textAllCaps="true"
         android:textColor="@android:color/white"
-        android:textSize="12sp" />
+        android:textSize="12sp"
+        android:focusable="true" />
 
     <TextView
         android:id="@+id/settings_button"
@@ -61,6 +63,7 @@
         android:text="@string/settings_button_text"
         android:textAllCaps="true"
         android:textColor="@android:color/white"
-        android:textSize="12sp" />
+        android:textSize="12sp"
+        android:focusable="true" />
 
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dummy_widget.xml b/res/layout/zzz_dummy_widget.xml
similarity index 100%
rename from res/layout/dummy_widget.xml
rename to res/layout/zzz_dummy_widget.xml
diff --git a/res/layout/zzz_weight_watcher.xml b/res/layout/zzz_weight_watcher.xml
new file mode 100644
index 0000000..07fd39e
--- /dev/null
+++ b/res/layout/zzz_weight_watcher.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.launcher3.testing.WeightWatcher xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 9f65569..af4c96d 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -66,9 +66,6 @@
 <!-- Hotseat -->
     <bool name="hotseat_transpose_layout_with_orientation">true</bool>
 
-    <!-- Memory debugging, including a memory dump icon -->
-    <bool name="debug_memory_enabled">false</bool>
-
     <!-- Name of a subclass of com.android.launcher3.AppFilter used to
          filter the activities shown in the launcher. Can be empty. -->
     <string name="app_filter_class" translatable="false"></string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index fefadef..f09f72a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -45,11 +45,11 @@
     <!-- SafeMode widget error string -->
     <string name="safemode_widget_error">Widgets disabled in Safe mode</string>
 
-    <string name="toggle_weight_watcher">Show Mem</string>
-
     <!-- Widgets -->
     <!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->
     <string name="long_press_widget_to_add">Touch &amp; hold to pick up a widget.</string>
+    <!-- Accessibility spoken hint message in widget picker, which allows user to add a widget. Custom action is the label for additional accessibility actions available in this mode [CHAR_LIMIT=100] -->
+    <string name="long_accessible_way_to_add">Double-tap &amp; hold to pick up a widget or use custom actions.</string>
     <!-- The format string for the dimensions of a widget in the drawer -->
     <!-- There is a special version of this format string for Farsi -->
     <string name="widget_dims_format">%1$d \u00d7 %2$d</string>
@@ -125,6 +125,8 @@
     <string name="default_scroll_format">Page %1$d of %2$d</string>
     <!-- The format string for Workspace page scroll text [CHAR_LIMIT=none] -->
     <string name="workspace_scroll_format">Home screen %1$d of %2$d</string>
+    <!-- Description for a new page on homescreen[CHAR_LIMIT=none] -->
+    <string name="workspace_new_page">New home screen page</string>
 
     <!-- Clings -->
     <!-- The title text for the workspace cling [CHAR_LIMIT=30] -->
@@ -158,9 +160,6 @@
     <!-- Folder name format -->
     <string name="folder_name_format">Folder: <xliff:g id="name" example="Games">%1$s</xliff:g></string>
 
-    <!-- Debug-only activity name. [DO NOT TRANSLATE] -->
-    <string name="debug_memory_activity">* HPROF</string>
-
     <!-- Strings for the customization mode -->
     <!-- Text for widget add button -->
     <string name="widget_button_text">Widgets</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 7d60cbe..4eee130 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -44,6 +44,8 @@
         <item name="android:textColor">@color/quantum_panel_text_color</item>
         <item name="android:drawablePadding">@dimen/dynamic_grid_icon_drawable_padding</item>
         <item name="android:shadowRadius">0</item>
+        <item name="android:paddingLeft">4dp</item>
+        <item name="android:paddingRight">4dp</item>
         <item name="customShadows">false</item>
     </style>
 
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 6818929..7bd5284 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -17,6 +17,8 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
+import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+
 public class AppWidgetResizeFrame extends FrameLayout {
     private static final int SNAP_DURATION = 150;
     private static final float DIMMED_HANDLE_ALPHA = 0f;
@@ -46,6 +48,8 @@
     private final int[] mLastDirectionVector = new int[2];
     private final int[] mTmpPt = new int[2];
 
+    private final DragViewStateAnnouncer mStateAnnouncer;
+
     private boolean mLeftBorderActive;
     private boolean mRightBorderActive;
     private boolean mTopBorderActive;
@@ -84,6 +88,8 @@
         mMinHSpan = info.minSpanX;
         mMinVSpan = info.minSpanY;
 
+        mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
+
         setBackgroundResource(R.drawable.widget_resize_shadow);
         setForeground(getResources().getDrawable(R.drawable.widget_resize_frame));
         setPadding(0, 0, 0, 0);
@@ -326,12 +332,18 @@
 
         if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
                 mDirectionVector, onDismiss)) {
+            if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
+                mStateAnnouncer.announce(
+                        mLauncher.getString(R.string.widget_resized, spanX, spanY));
+            }
+
             lp.tmpCellX = cellX;
             lp.tmpCellY = cellY;
             lp.cellHSpan = spanX;
             lp.cellVSpan = spanY;
             mRunningVInc += vSpanDelta;
             mRunningHInc += hSpanDelta;
+
             if (!onDismiss) {
                 updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
             }
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 10fdd87..54ce0fd 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -89,7 +89,7 @@
                     }
                     return null;
                 }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
+            }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
         }
 
         LauncherAppState app = LauncherAppState.getInstanceNoCreate();
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 9d713e3..f8ef1e1 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -52,8 +52,8 @@
         public int rowIndex;
         // The offset of the first visible row
         public int rowTopOffset;
-        // The height of a given row (they are currently all the same height)
-        public int rowHeight;
+        // The adapter position of the first visible item
+        public int itemPos;
     }
 
     protected BaseRecyclerViewFastScrollBar mScrollbar;
@@ -187,15 +187,21 @@
     }
 
     /**
+     * Returns the visible height of the recycler view:
+     *   VisibleHeight = View height - top padding - bottom padding
+     */
+    protected int getVisibleHeight() {
+        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
+        return visibleHeight;
+    }
+
+    /**
      * Returns the available scroll height:
      *   AvailableScrollHeight = Total height of the all items - last page height
-     *
-     * This assumes that all rows are the same height.
      */
-    protected int getAvailableScrollHeight(int rowCount, int rowHeight) {
-        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
-        int scrollHeight = getPaddingTop() + rowCount * rowHeight + getPaddingBottom();
-        int availableScrollHeight = scrollHeight - visibleHeight;
+    protected int getAvailableScrollHeight(int rowCount) {
+        int totalHeight = getPaddingTop() + getTop(rowCount) + getPaddingBottom();
+        int availableScrollHeight = totalHeight - getVisibleHeight();
         return availableScrollHeight;
     }
 
@@ -204,8 +210,7 @@
      *   AvailableScrollBarHeight = Total height of the visible view - thumb height
      */
     protected int getAvailableScrollBarHeight() {
-        int visibleHeight = getHeight() - mBackgroundPadding.top - mBackgroundPadding.bottom;
-        int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight();
+        int availableScrollBarHeight = getVisibleHeight() - mScrollbar.getThumbHeight();
         return availableScrollBarHeight;
     }
 
@@ -223,6 +228,13 @@
         return defaultInactiveThumbColor;
     }
 
+    /**
+     * Returns the scrollbar for this recycler view.
+     */
+    public BaseRecyclerViewFastScrollBar getScrollBar() {
+        return mScrollbar;
+    }
+
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
@@ -243,7 +255,7 @@
             int rowCount) {
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(rowCount, scrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(rowCount);
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -252,8 +264,7 @@
         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
         // padding)
-        int scrollY = getPaddingTop() +
-                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
+        int scrollY = getScrollTop(scrollPosState);
         int scrollBarY = mBackgroundPadding.top +
                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
 
@@ -268,7 +279,7 @@
     }
 
     /**
-     * Returns whether fast scrolling is supported in the current state.
+     * @return whether fast scrolling is supported in the current state.
      */
     protected boolean supportsFastScrolling() {
         return true;
@@ -277,22 +288,38 @@
     /**
      * Maps the touch (from 0..1) to the adapter position that should be visible.
      * <p>Override in each subclass of this base class.
+     *
+     * @return the scroll top of this recycler view.
      */
-    public abstract String scrollToPositionAtProgress(float touchFraction);
+    protected int getScrollTop(ScrollPositionState scrollPosState) {
+        return getPaddingTop() + getTop(scrollPosState.rowIndex) -
+                scrollPosState.rowTopOffset;
+    }
+
+    /**
+     * Returns information about the item that the recycler view is currently scrolled to.
+     */
+    protected abstract void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask);
+
+    /**
+     * Returns the top (or y position) of the row at the specified index.
+     */
+    protected abstract int getTop(int rowIndex);
+
+    /**
+     * Maps the touch (from 0..1) to the adapter position that should be visible.
+     * <p>Override in each subclass of this base class.
+     */
+    protected abstract String scrollToPositionAtProgress(float touchFraction);
 
     /**
      * Updates the bounds for the scrollbar.
      * <p>Override in each subclass of this base class.
      */
-    public abstract void onUpdateScrollbar(int dy);
+    protected abstract void onUpdateScrollbar(int dy);
 
     /**
      * <p>Override in each subclass of this base class.
      */
-    public void onFastScrollCompleted() {}
-
-    /**
-     * Returns information about the item that the recycler view is currently scrolled to.
-     */
-    protected abstract void getCurScrollState(ScrollPositionState stateOut);
+    protected void onFastScrollCompleted() {}
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
index 32ea576..a680169 100644
--- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
+++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java
@@ -27,6 +27,7 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 
 import com.android.launcher3.util.Thunk;
@@ -37,7 +38,7 @@
 public class BaseRecyclerViewFastScrollBar {
 
     public interface FastScrollFocusableView {
-        void setFastScrollFocused(boolean focused, boolean animated);
+        void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
     }
 
     private final static int MAX_TRACK_ALPHA = 30;
@@ -199,7 +200,7 @@
                     }
                     mTouchOffset += (lastY - downY);
                     mPopup.animateVisibility(true);
-                    animateScrollbar(true);
+                    showActiveScrollbar(true);
                 }
                 if (mIsDragging) {
                     // Update the fastscroller section name at this touch position
@@ -210,7 +211,7 @@
                             (bottom - top));
                     mPopup.setSectionName(sectionName);
                     mPopup.animateVisibility(!sectionName.isEmpty());
-                    mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY));
+                    mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
                     mLastTouchY = boundedY;
                 }
                 break;
@@ -222,7 +223,7 @@
                 if (mIsDragging) {
                     mIsDragging = false;
                     mPopup.animateVisibility(false);
-                    animateScrollbar(false);
+                    showActiveScrollbar(false);
                 }
                 break;
         }
@@ -246,7 +247,7 @@
     /**
      * Animates the width and color of the scrollbar.
      */
-    private void animateScrollbar(boolean isScrolling) {
+    private void showActiveScrollbar(boolean isScrolling) {
         if (mScrollbarAnimator != null) {
             mScrollbarAnimator.cancel();
         }
diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
index aeeb515..ebaba18 100644
--- a/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
+++ b/src/com/android/launcher3/BaseRecyclerViewFastScrollPopup.java
@@ -77,26 +77,26 @@
      * Updates the bounds for the fast scroller.
      * @return the invalidation rect for this update.
      */
-    public Rect updateFastScrollerBounds(BaseRecyclerView rv, int lastTouchY) {
+    public Rect updateFastScrollerBounds(int lastTouchY) {
         mInvalidateRect.set(mBgBounds);
 
         if (isVisible()) {
             // Calculate the dimensions and position of the fast scroller popup
-            int edgePadding = rv.getMaxScrollbarWidth();
+            int edgePadding = mRv.getMaxScrollbarWidth();
             int bgPadding = (mBgOriginalSize - mTextBounds.height()) / 2;
             int bgHeight = mBgOriginalSize;
             int bgWidth = Math.max(mBgOriginalSize, mTextBounds.width() + (2 * bgPadding));
             if (Utilities.isRtl(mRes)) {
-                mBgBounds.left = rv.getBackgroundPadding().left + (2 * rv.getMaxScrollbarWidth());
+                mBgBounds.left = mRv.getBackgroundPadding().left + (2 * mRv.getMaxScrollbarWidth());
                 mBgBounds.right = mBgBounds.left + bgWidth;
             } else {
-                mBgBounds.right = rv.getWidth() - rv.getBackgroundPadding().right -
-                        (2 * rv.getMaxScrollbarWidth());
+                mBgBounds.right = mRv.getWidth() - mRv.getBackgroundPadding().right -
+                        (2 * mRv.getMaxScrollbarWidth());
                 mBgBounds.left = mBgBounds.right - bgWidth;
             }
             mBgBounds.top = lastTouchY - (int) (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * bgHeight);
             mBgBounds.top = Math.max(edgePadding,
-                    Math.min(mBgBounds.top, rv.getHeight() - edgePadding - bgHeight));
+                    Math.min(mBgBounds.top, mRv.getHeight() - edgePadding - bgHeight));
             mBgBounds.bottom = mBgBounds.top + bgHeight;
         } else {
             mBgBounds.setEmpty();
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index c8af600..db1e4f5 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -16,7 +16,6 @@
 
 package com.android.launcher3;
 
-import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -25,7 +24,6 @@
 import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Paint;
 import android.graphics.Region;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
@@ -37,8 +35,6 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
 import android.widget.TextView;
 
 import com.android.launcher3.IconCache.IconLoadRequest;
@@ -63,13 +59,6 @@
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
 
-    private static final float FAST_SCROLL_FOCUS_MAX_SCALE = 1.15f;
-    private static final int FAST_SCROLL_FOCUS_MODE_NONE = 0;
-    private static final int FAST_SCROLL_FOCUS_MODE_SCALE_ICON = 1;
-    private static final int FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG = 2;
-    private static final int FAST_SCROLL_FOCUS_FADE_IN_DURATION = 175;
-    private static final int FAST_SCROLL_FOCUS_FADE_OUT_DURATION = 125;
-
     private final Launcher mLauncher;
     private Drawable mIcon;
     private final Drawable mBackground;
@@ -93,12 +82,6 @@
     private boolean mIgnorePressedStateChange;
     private boolean mDisableRelayout = false;
 
-    private ObjectAnimator mFastScrollFocusAnimator;
-    private Paint mFastScrollFocusBgPaint;
-    private float mFastScrollFocusFraction;
-    private boolean mFastScrollFocused;
-    private final int mFastScrollMode = FAST_SCROLL_FOCUS_MODE_SCALE_ICON;
-
     private IconLoadRequest mIconLoadRequest;
 
     public BubbleTextView(Context context) {
@@ -151,13 +134,6 @@
             setShadowLayer(SHADOW_LARGE_RADIUS, 0.0f, SHADOW_Y_OFFSET, SHADOW_LARGE_COLOUR);
         }
 
-        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG) {
-            mFastScrollFocusBgPaint = new Paint();
-            mFastScrollFocusBgPaint.setAntiAlias(true);
-            mFastScrollFocusBgPaint.setColor(
-                    getResources().getColor(R.color.container_fastscroll_thumb_active_color));
-        }
-
         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
     }
 
@@ -170,8 +146,9 @@
         Bitmap b = info.getIcon(iconCache);
 
         FastBitmapDrawable iconDrawable = mLauncher.createIconDrawable(b);
-        iconDrawable.setGhostModeEnabled(info.isDisabled != 0);
-
+        if (info.isDisabled != 0) {
+            iconDrawable.setState(FastBitmapDrawable.State.DISABLED);
+        }
         setIcon(iconDrawable, mIconSize);
         if (info.contentDescription != null) {
             setContentDescription(info.contentDescription);
@@ -259,7 +236,12 @@
 
     private void updateIconState() {
         if (mIcon instanceof FastBitmapDrawable) {
-            ((FastBitmapDrawable) mIcon).setPressed(isPressed() || mStayPressed);
+            FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
+            if (isPressed() || mStayPressed) {
+                d.animateState(FastBitmapDrawable.State.PRESSED);
+            } else {
+                d.animateState(FastBitmapDrawable.State.NORMAL);
+            }
         }
     }
 
@@ -362,18 +344,7 @@
     @Override
     public void draw(Canvas canvas) {
         if (!mCustomShadowsEnabled) {
-            // Draw the fast scroll focus bg if we have one
-            if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_DRAW_CIRCLE_BG &&
-                    mFastScrollFocusFraction > 0f) {
-                DeviceProfile grid = mLauncher.getDeviceProfile();
-                int iconCenterX = getScrollX() + (getWidth() / 2);
-                int iconCenterY = getScrollY() + getPaddingTop() + (grid.iconSizePx / 2);
-                canvas.drawCircle(iconCenterX, iconCenterY,
-                        mFastScrollFocusFraction * (getWidth() / 2), mFastScrollFocusBgPaint);
-            }
-
             super.draw(canvas);
-
             return;
         }
 
@@ -533,8 +504,13 @@
      */
     public void reapplyItemInfo(final ItemInfo info) {
         if (getTag() == info) {
+            FastBitmapDrawable.State prevState = FastBitmapDrawable.State.NORMAL;
+            if (mIcon instanceof FastBitmapDrawable) {
+                prevState = ((FastBitmapDrawable) mIcon).getCurrentState();
+            }
             mIconLoadRequest = null;
             mDisableRelayout = true;
+
             if (info instanceof AppInfo) {
                 applyFromApplicationInfo((AppInfo) info);
             } else if (info instanceof ShortcutInfo) {
@@ -550,6 +526,13 @@
             } else if (info instanceof PackageItemInfo) {
                 applyFromPackageItemInfo((PackageItemInfo) info);
             }
+
+            // If we are reapplying over an old icon, then we should update the new icon to the same
+            // state as the old icon
+            if (mIcon instanceof FastBitmapDrawable) {
+                ((FastBitmapDrawable) mIcon).setState(prevState);
+            }
+
             mDisableRelayout = false;
         }
     }
@@ -583,55 +566,53 @@
         }
     }
 
-    // Setters & getters for the animation
-    public void setFastScrollFocus(float fraction) {
-        mFastScrollFocusFraction = fraction;
-        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_SCALE_ICON) {
-            setScaleX(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f));
-            setScaleY(1f + fraction * (FAST_SCROLL_FOCUS_MAX_SCALE - 1f));
-        } else {
-            invalidate();
-        }
-    }
-
-    public float getFastScrollFocus() {
-        return mFastScrollFocusFraction;
-    }
-
     @Override
-    public void setFastScrollFocused(final boolean focused, boolean animated) {
-        if (mFastScrollMode == FAST_SCROLL_FOCUS_MODE_NONE) {
+    public void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated) {
+        // We can only set the fast scroll focus state on a FastBitmapDrawable
+        if (!(mIcon instanceof FastBitmapDrawable)) {
             return;
         }
 
-        if (mFastScrollFocused != focused) {
-            mFastScrollFocused = focused;
-
-            if (animated) {
-                // Clean up the previous focus animator
-                if (mFastScrollFocusAnimator != null) {
-                    mFastScrollFocusAnimator.cancel();
-                }
-                mFastScrollFocusAnimator = ObjectAnimator.ofFloat(this, "fastScrollFocus",
-                        focused ? 1f : 0f);
-                if (focused) {
-                    mFastScrollFocusAnimator.setInterpolator(new DecelerateInterpolator());
-                } else {
-                    mFastScrollFocusAnimator.setInterpolator(new AccelerateInterpolator());
-                }
-                mFastScrollFocusAnimator.setDuration(focused ?
-                        FAST_SCROLL_FOCUS_FADE_IN_DURATION : FAST_SCROLL_FOCUS_FADE_OUT_DURATION);
-                mFastScrollFocusAnimator.start();
-            } else {
-                mFastScrollFocusFraction = focused ? 1f : 0f;
+        FastBitmapDrawable d = (FastBitmapDrawable) mIcon;
+        if (animated) {
+            FastBitmapDrawable.State prevState = d.getCurrentState();
+            if (d.animateState(focusState)) {
+                // If the state was updated, then update the view accordingly
+                animate().scaleX(focusState.viewScale)
+                        .scaleY(focusState.viewScale)
+                        .setStartDelay(getStartDelayForStateChange(prevState, focusState))
+                        .setDuration(d.getDurationForStateChange(prevState, focusState))
+                        .start();
+            }
+        } else {
+            if (d.setState(focusState)) {
+                // If the state was updated, then update the view accordingly
+                animate().cancel();
+                setScaleX(focusState.viewScale);
+                setScaleY(focusState.viewScale);
             }
         }
     }
 
     /**
+     * Returns the start delay when animating between certain {@link FastBitmapDrawable} states.
+     */
+    private static int getStartDelayForStateChange(final FastBitmapDrawable.State fromState,
+            final FastBitmapDrawable.State toState) {
+        switch (toState) {
+            case NORMAL:
+                switch (fromState) {
+                    case FAST_SCROLL_HIGHLIGHTED:
+                        return FastBitmapDrawable.FAST_SCROLL_INACTIVE_DURATION / 4;
+                }
+        }
+        return 0;
+    }
+
+    /**
      * Interface to be implemented by the grand parent to allow click shadow effect.
      */
-    public static interface BubbleTextShadowHandler {
+    public interface BubbleTextShadowHandler {
         void setPressedIcon(BubbleTextView icon, Bitmap background);
     }
 }
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 703de53..2a8567c 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -34,6 +34,7 @@
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.LinearInterpolator;
 import android.widget.TextView;
@@ -126,6 +127,10 @@
             mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter));
             setTextColor(mHoverColor);
         }
+        if (d.stateAnnouncer != null) {
+            d.stateAnnouncer.cancel();
+        }
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
     }
 
     @Override
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 700bf9e..92e4043 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -1042,7 +1042,7 @@
     }
 
     void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX,
-            int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) {
+            int cellY, int spanX, int spanY, boolean resize, DropTarget.DragObject dragObject) {
         final int oldDragCellX = mDragCell[0];
         final int oldDragCellY = mDragCell[1];
 
@@ -1051,6 +1051,9 @@
         }
 
         if (cellX != oldDragCellX || cellY != oldDragCellY) {
+            Point dragOffset = dragObject.dragView.getDragVisualizeOffset();
+            Rect dragRegion = dragObject.dragView.getDragRegion();
+
             mDragCell[0] = cellX;
             mDragCell[1] = cellY;
 
@@ -1106,6 +1109,18 @@
             Utilities.scaleRectAboutCenter(r, getChildrenScale());
             mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline);
             mDragOutlineAnims[mDragOutlineCurrent].animateIn();
+
+            if (dragObject.stateAnnouncer != null) {
+                String msg;
+                if (isHotseat()) {
+                    msg = getContext().getString(R.string.move_to_hotseat_position,
+                            Math.max(cellX, cellY) + 1);
+                } else {
+                    msg = getContext().getString(R.string.move_to_empty_cell,
+                            cellY + 1, cellX + 1);
+                }
+                dragObject.stateAnnouncer.announce(msg);
+            }
         }
     }
 
@@ -2685,6 +2700,7 @@
             LayoutParams lp = (LayoutParams) child.getLayoutParams();
             lp.dropped = true;
             child.requestLayout();
+            markCellsAsOccupiedForView(child);
         }
     }
 
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 6d8fa6b..3a79d94 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -67,13 +67,14 @@
 
     /**
      * Removes the item from the workspace. If the view is not null, it also removes the view.
-     * @return true if the item was removed.
      */
-    public static boolean removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) {
-        // Remove the item from launcher and the db
+    public static void removeWorkspaceOrFolderItem(Launcher launcher, ItemInfo item, View view) {
+        // Remove the item from launcher and the db, we can ignore the containerInfo in this call
+        // because we already remove the drag view from the folder (if the drag originated from
+        // a folder) in Folder.beginDrag()
         launcher.removeItem(view, item, true /* deleteFromDb */);
         launcher.getWorkspace().stripEmptyScreens();
-        return true;
+        launcher.getDragLayer().announceForAccessibility(launcher.getString(R.string.item_removed));
     }
 
     @Override
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index 592cd32..bab46d9 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -21,6 +21,8 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 
+import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+
 /**
  * Interface defining an object that can receive a drag.
  *
@@ -66,6 +68,8 @@
         /** Defers removing the DragView from the DragLayer until after the drop animation. */
         public boolean deferDragViewCleanupPostAnimation = true;
 
+        public DragViewStateAnnouncer stateAnnouncer;
+
         public DragObject() {
         }
 
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index 28e923e..30bc7ea 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.graphics.Bitmap;
@@ -28,13 +29,40 @@
 import android.graphics.PixelFormat;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.SparseArray;
+import android.view.animation.DecelerateInterpolator;
 
 public class FastBitmapDrawable extends Drawable {
 
-    static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
+    /**
+     * The possible states that a FastBitmapDrawable can be in.
+     */
+    public enum State {
+
+        NORMAL                      (0f, 0f, 1f, new DecelerateInterpolator()),
+        PRESSED                     (0f, 100f / 255f, 1f, CLICK_FEEDBACK_INTERPOLATOR),
+        FAST_SCROLL_HIGHLIGHTED     (0f, 0f, 1.1f, new DecelerateInterpolator()),
+        FAST_SCROLL_UNHIGHLIGHTED   (0.8f, 0.35f, 1f, new DecelerateInterpolator()),
+        DISABLED                    (1f, 0.5f, 1f, new DecelerateInterpolator());
+
+        public final float desaturation;
+        public final float brightness;
+        /**
+         * Used specifically by the view drawing this FastBitmapDrawable.
+         */
+        public final float viewScale;
+        public final TimeInterpolator interpolator;
+
+        State(float desaturation, float brightness, float viewScale, TimeInterpolator interpolator) {
+            this.desaturation = desaturation;
+            this.brightness = brightness;
+            this.viewScale = viewScale;
+            this.interpolator = interpolator;
+        }
+    }
+
+    public static final TimeInterpolator CLICK_FEEDBACK_INTERPOLATOR = new TimeInterpolator() {
 
         @Override
         public float getInterpolation(float input) {
@@ -47,42 +75,46 @@
             }
         }
     };
-    static final long CLICK_FEEDBACK_DURATION = 2000;
+    public static final int CLICK_FEEDBACK_DURATION = 2000;
+    public static final int FAST_SCROLL_HIGHLIGHT_DURATION = 225;
+    public static final int FAST_SCROLL_UNHIGHLIGHT_DURATION = 150;
+    public static final int FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION = 225;
+    public static final int FAST_SCROLL_INACTIVE_DURATION = 275;
 
-    private static final int PRESSED_BRIGHTNESS = 100;
-    private static ColorMatrix sGhostModeMatrix;
-    private static final ColorMatrix sTempMatrix = new ColorMatrix();
+    // Since we don't need 256^2 values for combinations of both the brightness and saturation, we
+    // reduce the value space to a smaller value V, which reduces the number of cached
+    // ColorMatrixColorFilters that we need to keep to V^2
+    private static final int REDUCED_FILTER_VALUE_SPACE = 48;
 
-    /**
-     * Store the brightness colors filters to optimize animations during icon press. This
-     * only works for non-ghost-mode icons.
-     */
-    private static final SparseArray<ColorFilter> sCachedBrightnessFilter =
-            new SparseArray<ColorFilter>();
+    // A cache of ColorFilters for optimizing brightness and saturation animations
+    private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>();
 
-    private static final int GHOST_MODE_MIN_COLOR_RANGE = 130;
+    // Temporary matrices used for calculation
+    private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix();
+    private static final ColorMatrix sTempFilterMatrix = new ColorMatrix();
 
     private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
     private final Bitmap mBitmap;
-    private int mAlpha;
+    private State mState = State.NORMAL;
 
+    // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and
+    // as a result, can be used to compose the key for the cached ColorMatrixColorFilters
+    private int mDesaturation = 0;
     private int mBrightness = 0;
-    private boolean mGhostModeEnabled = false;
+    private int mAlpha = 255;
+    private int mPrevUpdateKey = Integer.MAX_VALUE;
 
-    private boolean mPressed = false;
-    private ObjectAnimator mPressedAnimator;
+    // Animators for the fast bitmap drawable's properties
+    private AnimatorSet mPropertyAnimator;
 
     public FastBitmapDrawable(Bitmap b) {
-        mAlpha = 255;
         mBitmap = b;
         setBounds(0, 0, b.getWidth(), b.getHeight());
     }
 
     @Override
     public void draw(Canvas canvas) {
-        final Rect r = getBounds();
-        // Draw the bitmap into the bounding rect
-        canvas.drawBitmap(mBitmap, null, r, mPaint);
+        canvas.drawBitmap(mBitmap, null, getBounds(), mPaint);
     }
 
     @Override
@@ -136,96 +168,191 @@
     }
 
     /**
-     * When enabled, the icon is grayed out and the contrast is increased to give it a 'ghost'
-     * appearance.
+     * Animates this drawable to a new state.
+     *
+     * @return whether the state has changed.
      */
-    public void setGhostModeEnabled(boolean enabled) {
-        if (mGhostModeEnabled != enabled) {
-            mGhostModeEnabled = enabled;
+    public boolean animateState(State newState) {
+        State prevState = mState;
+        if (mState != newState) {
+            mState = newState;
+
+            mPropertyAnimator = cancelAnimator(mPropertyAnimator);
+            mPropertyAnimator = new AnimatorSet();
+            mPropertyAnimator.playTogether(
+                    ObjectAnimator
+                            .ofFloat(this, "desaturation", newState.desaturation),
+                    ObjectAnimator
+                            .ofFloat(this, "brightness", newState.brightness));
+            mPropertyAnimator.setInterpolator(newState.interpolator);
+            mPropertyAnimator.setDuration(getDurationForStateChange(prevState, newState));
+            mPropertyAnimator.setStartDelay(getStartDelayForStateChange(prevState, newState));
+            mPropertyAnimator.start();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Immediately sets this drawable to a new state.
+     *
+     * @return whether the state has changed.
+     */
+    public boolean setState(State newState) {
+        if (mState != newState) {
+            mState = newState;
+
+            mPropertyAnimator = cancelAnimator(mPropertyAnimator);
+
+            setDesaturation(newState.desaturation);
+            setBrightness(newState.brightness);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the current state.
+     */
+    public State getCurrentState() {
+        return mState;
+    }
+
+    /**
+     * Returns the duration for the state change animation.
+     */
+    public static int getDurationForStateChange(State fromState, State toState) {
+        switch (toState) {
+            case NORMAL:
+                switch (fromState) {
+                    case PRESSED:
+                        return 0;
+                    case FAST_SCROLL_HIGHLIGHTED:
+                    case FAST_SCROLL_UNHIGHLIGHTED:
+                        return FAST_SCROLL_INACTIVE_DURATION;
+                }
+            case PRESSED:
+                return CLICK_FEEDBACK_DURATION;
+            case FAST_SCROLL_HIGHLIGHTED:
+                return FAST_SCROLL_HIGHLIGHT_DURATION;
+            case FAST_SCROLL_UNHIGHLIGHTED:
+                switch (fromState) {
+                    case NORMAL:
+                        // When animating from normal state, take a little longer
+                        return FAST_SCROLL_UNHIGHLIGHT_FROM_NORMAL_DURATION;
+                    default:
+                        return FAST_SCROLL_UNHIGHLIGHT_DURATION;
+                }
+        }
+        return 0;
+    }
+
+    /**
+     * Returns the start delay when animating between certain fast scroll states.
+     */
+    public static int getStartDelayForStateChange(State fromState, State toState) {
+        switch (toState) {
+            case FAST_SCROLL_UNHIGHLIGHTED:
+                switch (fromState) {
+                    case NORMAL:
+                        return FAST_SCROLL_UNHIGHLIGHT_DURATION / 4;
+                }
+        }
+        return 0;
+    }
+
+    /**
+     * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated]
+     */
+    public void setDesaturation(float desaturation) {
+        int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE);
+        if (mDesaturation != newDesaturation) {
+            mDesaturation = newDesaturation;
             updateFilter();
         }
     }
 
-    public void setPressed(boolean pressed) {
-        if (mPressed != pressed) {
-            mPressed = pressed;
-            if (mPressed) {
-                mPressedAnimator = ObjectAnimator
-                        .ofInt(this, "brightness", PRESSED_BRIGHTNESS)
-                        .setDuration(CLICK_FEEDBACK_DURATION);
-                mPressedAnimator.setInterpolator(CLICK_FEEDBACK_INTERPOLATOR);
-                mPressedAnimator.start();
-            } else if (mPressedAnimator != null) {
-                mPressedAnimator.cancel();
-                setBrightness(0);
-            }
-        }
-        invalidateSelf();
+    public float getDesaturation() {
+        return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE;
     }
 
-    public boolean isGhostModeEnabled() {
-        return mGhostModeEnabled;
-    }
-
-    public int getBrightness() {
-        return mBrightness;
-    }
-
-    public void setBrightness(int brightness) {
-        if (mBrightness != brightness) {
-            mBrightness = brightness;
+    /**
+     * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious]
+     */
+    public void setBrightness(float brightness) {
+        int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE);
+        if (mBrightness != newBrightness) {
+            mBrightness = newBrightness;
             updateFilter();
-            invalidateSelf();
         }
     }
 
+    public float getBrightness() {
+        return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE;
+    }
+
+    /**
+     * Updates the paint to reflect the current brightness and saturation.
+     */
     private void updateFilter() {
-        if (mGhostModeEnabled) {
-            if (sGhostModeMatrix == null) {
-                sGhostModeMatrix = new ColorMatrix();
-                sGhostModeMatrix.setSaturation(0);
+        boolean usePorterDuffFilter = false;
+        int key = -1;
+        if (mDesaturation > 0) {
+            key = (mDesaturation << 16) | mBrightness;
+        } else if (mBrightness > 0) {
+            // Compose a key with a fully saturated icon if we are just animating brightness
+            key = (1 << 16) | mBrightness;
 
-                // For ghost mode, set the color range to [GHOST_MODE_MIN_COLOR_RANGE, 255]
-                float range = (255 - GHOST_MODE_MIN_COLOR_RANGE) / 255.0f;
-                sTempMatrix.set(new float[] {
-                        range, 0, 0, 0, GHOST_MODE_MIN_COLOR_RANGE,
-                        0, range, 0, 0, GHOST_MODE_MIN_COLOR_RANGE,
-                        0, 0, range, 0, GHOST_MODE_MIN_COLOR_RANGE,
-                        0, 0, 0, 1, 0 });
-                sGhostModeMatrix.preConcat(sTempMatrix);
-            }
+            // We found that in L, ColorFilters cause drawing artifacts with shadows baked into
+            // icons, so just use a PorterDuff filter when we aren't animating saturation
+            usePorterDuffFilter = true;
+        }
 
-            if (mBrightness == 0) {
-                mPaint.setColorFilter(new ColorMatrixColorFilter(sGhostModeMatrix));
-            } else {
-                setBrightnessMatrix(sTempMatrix, mBrightness);
-                sTempMatrix.postConcat(sGhostModeMatrix);
-                mPaint.setColorFilter(new ColorMatrixColorFilter(sTempMatrix));
-            }
-        } else if (mBrightness != 0) {
-            ColorFilter filter = sCachedBrightnessFilter.get(mBrightness);
+        // Debounce multiple updates on the same frame
+        if (key == mPrevUpdateKey) {
+            return;
+        }
+        mPrevUpdateKey = key;
+
+        if (key != -1) {
+            ColorFilter filter = sCachedFilter.get(key);
             if (filter == null) {
-                filter = new PorterDuffColorFilter(Color.argb(mBrightness, 255, 255, 255),
-                        PorterDuff.Mode.SRC_ATOP);
-                sCachedBrightnessFilter.put(mBrightness, filter);
+                float brightnessF = getBrightness();
+                int brightnessI = (int) (255 * brightnessF);
+                if (usePorterDuffFilter) {
+                    filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255),
+                            PorterDuff.Mode.SRC_ATOP);
+                } else {
+                    float saturationF = 1f - getDesaturation();
+                    sTempFilterMatrix.setSaturation(saturationF);
+                    if (mBrightness > 0) {
+                        // Brightness: C-new = C-old*(1-amount) + amount
+                        float scale = 1f - brightnessF;
+                        float[] mat = sTempBrightnessMatrix.getArray();
+                        mat[0] = scale;
+                        mat[6] = scale;
+                        mat[12] = scale;
+                        mat[4] = brightnessI;
+                        mat[9] = brightnessI;
+                        mat[14] = brightnessI;
+                        sTempFilterMatrix.preConcat(sTempBrightnessMatrix);
+                    }
+                    filter = new ColorMatrixColorFilter(sTempFilterMatrix);
+                }
+                sCachedFilter.append(key, filter);
             }
             mPaint.setColorFilter(filter);
         } else {
             mPaint.setColorFilter(null);
         }
+        invalidateSelf();
     }
 
-    private static void setBrightnessMatrix(ColorMatrix matrix, int brightness) {
-        // Brightness: C-new = C-old*(1-amount) + amount
-        float scale = 1 - brightness / 255.0f;
-        matrix.setScale(scale, scale, scale, 1);
-        float[] array = matrix.getArray();
-
-        // Add the amount to RGB components of the matrix, as per the above formula.
-        // Fifth elements in the array correspond to the constant being added to
-        // red, blue, green, and alpha channel respectively.
-        array[4] = brightness;
-        array[9] = brightness;
-        array[14] = brightness;
+    private AnimatorSet cancelAnimator(AnimatorSet animator) {
+        if (animator != null) {
+            animator.removeAllListeners();
+            animator.cancel();
+        }
+        return null;
     }
 }
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index 6f34587..099194c 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -73,7 +73,6 @@
                         KeyEvent.keyCodeToString(keyCode)));
             }
 
-
             if (!(v.getParent() instanceof ShortcutAndWidgetContainer)) {
                 if (ProviderConfig.IS_DOGFOOD_BUILD) {
                     throw new IllegalStateException("Parent of the focused item is not supported.");
@@ -196,10 +195,11 @@
         }
 
         // Initialize the variables.
+        final Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
         final ShortcutAndWidgetContainer hotseatParent = (ShortcutAndWidgetContainer) v.getParent();
         final CellLayout hotseatLayout = (CellLayout) hotseatParent.getParent();
 
-        Workspace workspace = (Workspace) v.getRootView().findViewById(R.id.workspace);
+        final ItemInfo itemInfo = (ItemInfo) v.getTag();
         int pageIndex = workspace.getNextPage();
         int pageCount = workspace.getChildCount();
         int countX = -1;
@@ -241,9 +241,14 @@
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
                 profile.isVerticalBarLayout()) {
             keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
-        } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
-            ItemInfo info = (ItemInfo) v.getTag();
-            launcher.removeItem(v, info, true /* deleteFromDb */);
+        } else if (isUninstallKeyChord(e)) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout);
+            if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) {
+                UninstallDropTarget.startUninstallActivity(launcher, itemInfo);
+            }
+        } else if (isDeleteKeyChord(e)) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout);
+            launcher.removeItem(v, itemInfo, true /* deleteFromDb */);
         } else {
             // For other KEYCODE_DPAD_LEFT and KEYCODE_DPAD_RIGHT navigation, do not use the
             // matrix extended with hotseat.
@@ -305,6 +310,7 @@
         final ViewGroup tabs = (ViewGroup) dragLayer.findViewById(R.id.search_drop_target_bar);
         final Hotseat hotseat = (Hotseat) dragLayer.findViewById(R.id.hotseat);
 
+        final ItemInfo itemInfo = (ItemInfo) v.getTag();
         final int iconIndex = parent.indexOfChild(v);
         final int pageIndex = workspace.indexOfChild(iconLayout);
         final int pageCount = workspace.getChildCount();
@@ -329,10 +335,14 @@
                     profile.inv.hotseatAllAppsRank,
                     !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
             countX = countX + 1;
-        } else if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
-            ItemInfo info = (ItemInfo) v.getTag();
-            launcher.removeItem(v, info, true /* deleteFromDb */);
-            return consume;
+        } else if (isUninstallKeyChord(e)) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout);
+            if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) {
+                UninstallDropTarget.startUninstallActivity(launcher, itemInfo);
+            }
+        } else if (isDeleteKeyChord(e)) {
+            matrix = FocusLogic.createSparseMatrix(iconLayout);
+            launcher.removeItem(v, itemInfo, true /* deleteFromDb */);
         } else {
             matrix = FocusLogic.createSparseMatrix(iconLayout);
         }
@@ -462,4 +472,22 @@
                 break;
         }
     }
+
+    /**
+     * Returns whether the key event represents a valid uninstall key chord.
+     */
+    private static boolean isUninstallKeyChord(KeyEvent event) {
+        int keyCode = event.getKeyCode();
+        return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) &&
+                event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON);
+    }
+
+    /**
+     * Returns whether the key event represents a valid delete key chord.
+     */
+    private static boolean isDeleteKeyChord(KeyEvent event) {
+        int keyCode = event.getKeyCode();
+        return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) &&
+                event.hasModifiers(KeyEvent.META_CTRL_ON);
+    }
 }
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index e7b1902..3f9ffea 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -716,6 +716,11 @@
             mReorderAlarm.setOnAlarmListener(mReorderAlarmListener);
             mReorderAlarm.setAlarm(REORDER_DELAY);
             mPrevTargetRank = mTargetRank;
+
+            if (d.stateAnnouncer != null) {
+                d.stateAnnouncer.announce(getContext().getString(R.string.move_to_position,
+                        mTargetRank + 1));
+            }
         }
 
         float x = r[0];
@@ -1128,26 +1133,29 @@
             public void run() {
                 CellLayout cellLayout = mLauncher.getCellLayout(mInfo.container, mInfo.screenId);
 
-                View child = null;
-                // Move the item from the folder to the workspace, in the position of the folder
-                if (getItemCount() == 1) {
-                    ShortcutInfo finalItem = mInfo.contents.get(0);
-                    child = mLauncher.createShortcut(cellLayout, finalItem);
-                    LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
-                            mInfo.screenId, mInfo.cellX, mInfo.cellY);
-                }
+                // Remove the folder
                 if (getItemCount() <= 1) {
                     mLauncher.removeItem(mFolderIcon, mInfo, true /* deleteFromDb */);
                     if (mFolderIcon instanceof DropTarget) {
                         mDragController.removeDropTarget((DropTarget) mFolderIcon);
                     }
                 }
-                // We add the child after removing the folder to prevent both from existing at
-                // the same time in the CellLayout.  We need to add the new item with addInScreenFromBind()
-                // to ensure that hotseat items are placed correctly.
-                if (child != null) {
+
+                // Move the item from the folder to the workspace, in the position of the folder
+                if (getItemCount() == 1) {
+                    ShortcutInfo finalItem = mInfo.contents.get(0);
+                    View child = mLauncher.createShortcut(cellLayout, finalItem);
+                    LauncherModel.addOrMoveItemInDatabase(mLauncher, finalItem, mInfo.container,
+                            mInfo.screenId, mInfo.cellX, mInfo.cellY);
+
+                    // We add the child after removing the folder to prevent both from existing at
+                    // the same time in the CellLayout.  We need to add the new item with addInScreenFromBind()
+                    // to ensure that hotseat items are placed correctly.
                     mLauncher.getWorkspace().addInScreenFromBind(child, mInfo.container, mInfo.screenId,
                             mInfo.cellX, mInfo.cellY, mInfo.spanX, mInfo.spanY);
+
+                    // Focus the newly created child
+                    child.requestFocus();
                 }
             }
         };
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
index bd61a6d..07bd0aa 100644
--- a/src/com/android/launcher3/FolderIcon.java
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -521,7 +521,7 @@
     }
 
     class PreviewItemDrawingParams {
-        PreviewItemDrawingParams(float transX, float transY, float scale, int overlayAlpha) {
+        PreviewItemDrawingParams(float transX, float transY, float scale, float overlayAlpha) {
             this.transX = transX;
             this.transY = transY;
             this.scale = scale;
@@ -530,7 +530,7 @@
         float transX;
         float transY;
         float scale;
-        int overlayAlpha;
+        float overlayAlpha;
         Drawable drawable;
     }
 
@@ -562,7 +562,7 @@
         float transY = mAvailableSpaceInPreview - (offset + scaledSize + scaleOffsetCorrection) + getPaddingTop();
         float transX = (mAvailableSpaceInPreview - scaledSize) / 2;
         float totalScale = mBaselineIconScale * scale;
-        final int overlayAlpha = (int) (80 * (1 - r));
+        final float overlayAlpha = (80 * (1 - r)) / 255f;
 
         if (params == null) {
             params = new PreviewItemDrawingParams(transX, transY, totalScale, overlayAlpha);
@@ -586,12 +586,12 @@
             d.setBounds(0, 0, mIntrinsicIconSize, mIntrinsicIconSize);
             if (d instanceof FastBitmapDrawable) {
                 FastBitmapDrawable fd = (FastBitmapDrawable) d;
-                int oldBrightness = fd.getBrightness();
+                float oldBrightness = fd.getBrightness();
                 fd.setBrightness(params.overlayAlpha);
                 d.draw(canvas);
                 fd.setBrightness(oldBrightness);
             } else {
-                d.setColorFilter(Color.argb(params.overlayAlpha, 255, 255, 255),
+                d.setColorFilter(Color.argb((int) (params.overlayAlpha * 255), 255, 255, 255),
                         PorterDuff.Mode.SRC_ATOP);
                 d.draw(canvas);
                 d.clearColorFilter();
diff --git a/src/com/android/launcher3/HolographicOutlineHelper.java b/src/com/android/launcher3/HolographicOutlineHelper.java
index 5ff85d6..0d68e33 100644
--- a/src/com/android/launcher3/HolographicOutlineHelper.java
+++ b/src/com/android/launcher3/HolographicOutlineHelper.java
@@ -164,6 +164,9 @@
 
         int bitmapWidth = (int) (rect.width() * view.getScaleX());
         int bitmapHeight = (int) (rect.height() * view.getScaleY());
+        if (bitmapHeight <= 0 || bitmapWidth <= 0) {
+            return null;
+        }
 
         int key = (bitmapWidth << 16) | bitmapHeight;
         Bitmap cache = mBitmapCache.get(key);
diff --git a/src/com/android/launcher3/InfoDropTarget.java b/src/com/android/launcher3/InfoDropTarget.java
index 006ce5d..d444640 100644
--- a/src/com/android/launcher3/InfoDropTarget.java
+++ b/src/com/android/launcher3/InfoDropTarget.java
@@ -50,6 +50,8 @@
             componentName = ((ShortcutInfo) info).intent.getComponent();
         } else if (info instanceof PendingAddItemInfo) {
             componentName = ((PendingAddItemInfo) info).componentName;
+        } else if (info instanceof LauncherAppWidgetInfo) {
+            componentName = ((LauncherAppWidgetInfo) info).providerName;
         }
         if (componentName != null) {
             launcher.startApplicationDetailsActivity(componentName, info.user);
@@ -70,6 +72,6 @@
 
     public static boolean supportsDrop(Context context, ItemInfo info) {
         return info instanceof AppInfo || info instanceof ShortcutInfo
-                || info instanceof PendingAddItemInfo;
+                || info instanceof PendingAddItemInfo || info instanceof LauncherAppWidgetInfo;
     }
 }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index ae204c4..6881307 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -184,9 +184,9 @@
         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
                 296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
-                335, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
-        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
                 359, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
+        predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
+                335, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
         predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone",
                 406, 694,     5, 5, 4, 4, 4, 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/Launcher.java b/src/com/android/launcher3/Launcher.java
index 35d08ba..143d19b 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -46,6 +46,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Bitmap;
@@ -71,7 +72,6 @@
 import android.text.method.TextKeyListener;
 import android.util.Log;
 import android.view.Display;
-import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.Menu;
@@ -89,7 +89,6 @@
 import android.view.animation.OvershootInterpolator;
 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;
@@ -109,7 +108,9 @@
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.LongArrayMap;
+import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
 import com.android.launcher3.widget.WidgetHostViewLoader;
 import com.android.launcher3.widget.WidgetsContainerView;
@@ -190,9 +191,6 @@
     static final String ACTION_FIRST_LOAD_COMPLETE =
             "com.android.launcher3.action.FIRST_LOAD_COMPLETE";
 
-    public static final String SHOW_WEIGHT_WATCHER = "debug.show_mem";
-    public static final boolean SHOW_WEIGHT_WATCHER_DEFAULT = false;
-
     private static final String QSB_WIDGET_ID = "qsb_widget_id";
     private static final String QSB_WIDGET_PROVIDER = "qsb_widget_provider";
 
@@ -229,7 +227,8 @@
     private View mPageIndicators;
     @Thunk DragLayer mDragLayer;
     private DragController mDragController;
-    private View mWeightWatcher;
+
+    public View mWeightWatcher;
 
     private AppWidgetManagerCompat mAppWidgetManager;
     private LauncherAppWidgetHost mAppWidgetHost;
@@ -275,6 +274,7 @@
 
     private ArrayList<Runnable> mBindOnResumeCallbacks = new ArrayList<Runnable>();
     private ArrayList<Runnable> mOnResumeCallbacks = new ArrayList<Runnable>();
+    private ViewOnDrawExecutor mPendingExecutor;
 
     private LauncherModel mModel;
     private IconCache mIconCache;
@@ -328,6 +328,8 @@
 
     private DeviceProfile mDeviceProfile;
 
+    private boolean mMoveToDefaultScreenFromNewIntent;
+
     // This is set to the view that launched the activity that navigated the user away from
     // launcher. Since there is no callback for when the activity has finished launching, enable
     // the press state and keep this reference to reset the press state when we return to launcher.
@@ -336,10 +338,9 @@
     protected static HashMap<String, CustomAppWidget> sCustomAppWidgets =
             new HashMap<String, CustomAppWidget>();
 
-    private static final boolean ENABLE_CUSTOM_WIDGET_TEST = false;
     static {
-        if (ENABLE_CUSTOM_WIDGET_TEST) {
-            sCustomAppWidgets.put(DummyWidget.class.getName(), new DummyWidget());
+        if (TestingUtils.ENABLE_CUSTOM_WIDGET_TEST) {
+            TestingUtils.addDummyWidget(sCustomAppWidgets);
         }
     }
 
@@ -704,6 +705,9 @@
             return;
         } else if (requestCode == REQUEST_PICK_WALLPAPER) {
             if (resultCode == RESULT_OK && mWorkspace.isInOverviewMode()) {
+                // User could have free-scrolled between pages before picking a wallpaper; make sure
+                // we move to the closest one now.
+                mWorkspace.setCurrentPage(mWorkspace.getPageNearestToCenterOfScreen());
                 showWorkspace(false);
             }
             return;
@@ -950,12 +954,6 @@
         mPaused = false;
         if (mRestoring || mOnResumeNeedsLoad) {
             setWorkspaceLoading(true);
-
-            // If we're starting binding all over again, clear any bind calls we'd postponed in
-            // the past (see waitUntilResume) -- we don't need them since we're starting binding
-            // from scratch again
-            mBindOnResumeCallbacks.clear();
-
             mModel.startLoader(PagedView.INVALID_RESTORE_PAGE);
             mRestoring = false;
             mOnResumeNeedsLoad = false;
@@ -1002,14 +1000,21 @@
             Log.d(TAG, "Time spent in onResume: " + (System.currentTimeMillis() - startTime));
         }
 
-        if (mWorkspace.getCustomContentCallbacks() != null) {
+        // We want to suppress callbacks about CustomContent being shown if we have just received
+        // onNewIntent while the user was present within launcher. In that case, we post a call
+        // to move the user to the main screen (which will occur after onResume). We don't want to
+        // have onHide (from onPause), then onShow, then onHide again, which we get if we don't
+        // suppress here.
+        if (mWorkspace.getCustomContentCallbacks() != null
+                && !mMoveToDefaultScreenFromNewIntent) {
             // If we are resuming and the custom content is the current page, we call onShow().
-            // It is also poassible that onShow will instead be called slightly after first layout
+            // It is also possible that onShow will instead be called slightly after first layout
             // if PagedView#setRestorePage was set to the custom content page in onCreate().
             if (mWorkspace.isOnOrMovingToCustomContent()) {
                 mWorkspace.getCustomContentCallbacks().onShow(true);
             }
         }
+        mMoveToDefaultScreenFromNewIntent = false;
         updateInteraction(Workspace.State.NORMAL, mWorkspace.getState());
         mWorkspace.onResume();
 
@@ -1224,6 +1229,29 @@
         return handled;
     }
 
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_MENU) {
+            // Ignore the menu key if we are currently dragging or are on the custom content screen
+            if (!isOnCustomContent() && !mDragController.isDragging()) {
+                // Close any open folders
+                closeFolder();
+
+                // Stop resizing any widgets
+                mWorkspace.exitWidgetResizeMode();
+
+                // Show the overview mode if we are on the workspace
+                if (mState == State.WORKSPACE && !mWorkspace.isInOverviewMode() &&
+                        !mWorkspace.isSwitchingState()) {
+                    mOverviewPanel.requestFocus();
+                    showOverviewMode(true);
+                }
+            }
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
     private String getTypedText() {
         return mDefaultKeySsb.toString();
     }
@@ -1395,19 +1423,8 @@
             mAppInfoDropTargetBar.setup(this, dragController);
         }
 
-        if (getResources().getBoolean(R.bool.debug_memory_enabled)) {
-            Log.v(TAG, "adding WeightWatcher");
-            mWeightWatcher = new WeightWatcher(this);
-            mWeightWatcher.setAlpha(0.5f);
-            ((FrameLayout) mLauncherView).addView(mWeightWatcher,
-                    new FrameLayout.LayoutParams(
-                            FrameLayout.LayoutParams.MATCH_PARENT,
-                            FrameLayout.LayoutParams.WRAP_CONTENT,
-                            Gravity.BOTTOM)
-            );
-
-            boolean show = shouldShowWeightWatcher();
-            mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE);
+        if (TestingUtils.MEMORY_DUMP_ENABLED) {
+            TestingUtils.addWeightWatcher(this);
         }
     }
 
@@ -1877,6 +1894,10 @@
                     mLauncherCallbacks.shouldMoveToDefaultScreenOnHomeIntent() : true;
             if (alreadyOnHome && mState == State.WORKSPACE && !mWorkspace.isTouchActive() &&
                     openFolder == null && moveToDefaultScreen) {
+
+                // We use this flag to suppress noisy callbacks above custom content state
+                // from onResume.
+                mMoveToDefaultScreenFromNewIntent = true;
                 mWorkspace.post(new Runnable() {
                     @Override
                     public void run() {
@@ -2108,22 +2129,9 @@
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
-        if (!isOnCustomContent()) {
-            // Close any open folders
-            closeFolder();
-            // Stop resizing any widgets
-            mWorkspace.exitWidgetResizeMode();
-            if (!mWorkspace.isInOverviewMode()) {
-                // Show the overview mode
-                showOverviewMode(true);
-            } else {
-                showWorkspace(true);
-            }
-        }
         if (mLauncherCallbacks != null) {
             return mLauncherCallbacks.onPrepareOptionsMenu(menu);
         }
-
         return false;
     }
 
@@ -2330,13 +2338,21 @@
     }
 
     /**
-     * Unbinds the view for the specified item, and removes the item and all its children items
-     * from the database.  For folders, this incl udes the folder contents.  AppWidgets will also
-     * have their widget ids deleted.
+     * Unbinds the view for the specified item, and removes the item and all its children.
+     *
+     * @param v the view being removed.
+     * @param itemInfo the {@link ItemInfo} for this view.
+     * @param deleteFromDb whether or not to delete this item from the db.
      */
     public boolean removeItem(View v, ItemInfo itemInfo, boolean deleteFromDb) {
         if (itemInfo instanceof ShortcutInfo) {
-            mWorkspace.removeWorkspaceItem(v);
+            // Remove the shortcut from the folder before removing it from launcher
+            FolderInfo folderInfo = sFolders.get(itemInfo.container);
+            if (folderInfo != null) {
+                folderInfo.remove((ShortcutInfo) itemInfo);
+            } else {
+                mWorkspace.removeWorkspaceItem(v);
+            }
             if (deleteFromDb) {
                 LauncherModel.deleteItemFromDatabase(this, itemInfo);
             }
@@ -2380,7 +2396,7 @@
                     appWidgetHost.deleteAppWidgetId(widgetInfo.appWidgetId);
                     return null;
                 }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+            }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
         }
         removeWidgetToAutoAdvance(widgetInfo.hostView);
         widgetInfo.hostView = null;
@@ -2610,21 +2626,6 @@
             return;
         }
 
-        final Intent intent = shortcut.intent;
-
-        // Check for special shortcuts
-        if (intent.getComponent() != null) {
-            final String shortcutClass = intent.getComponent().getClassName();
-
-            if (shortcutClass.equals(MemoryDumpActivity.class.getName())) {
-                MemoryDumpActivity.startDump(this);
-                return;
-            } else if (shortcutClass.equals(ToggleWeightWatcher.class.getName())) {
-                toggleShowWeightWatcher();
-                return;
-            }
-        }
-
         // Check for abandoned promise
         if ((v instanceof BubbleTextView)
                 && shortcut.isPromise()
@@ -3471,8 +3472,12 @@
         // NO-OP
     }
 
+    public boolean launcherCallbacksProvidesSearch() {
+        return (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch());
+    }
+
     public View getOrCreateQsbBar() {
-        if (mLauncherCallbacks != null && mLauncherCallbacks.providesSearch()) {
+        if (launcherCallbacksProvidesSearch()) {
             return mLauncherCallbacks.getQsbBar();
         }
 
@@ -3636,6 +3641,19 @@
     }
 
     /**
+     * Clear any pending bind callbacks. This is called when is loader is planning to
+     * perform a full rebind from scratch.
+     */
+    @Override
+    public void clearPendingBinds() {
+        mBindOnResumeCallbacks.clear();
+        if (mPendingExecutor != null) {
+            mPendingExecutor.markCompleted();
+            mPendingExecutor = null;
+        }
+    }
+
+    /**
      * Refreshes the shortcuts shown on the workspace.
      *
      * Implementation of the method from LauncherModel.Callbacks.
@@ -3643,11 +3661,6 @@
     public void startBinding() {
         setWorkspaceLoading(true);
 
-        // If we're starting binding all over again, clear any bind calls we'd postponed in
-        // the past (see waitUntilResume) -- we don't need them since we're starting binding
-        // from scratch again
-        mBindOnResumeCallbacks.clear();
-
         // Clear the workspace because it's going to be rebound
         mWorkspace.clearDropTargets();
         mWorkspace.removeAllWorkspaceScreens();
@@ -3683,30 +3696,6 @@
         }
     }
 
-    private boolean shouldShowWeightWatcher() {
-        String spKey = LauncherAppState.getSharedPreferencesKey();
-        SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE);
-        boolean show = sp.getBoolean(SHOW_WEIGHT_WATCHER, SHOW_WEIGHT_WATCHER_DEFAULT);
-
-        return show;
-    }
-
-    private void toggleShowWeightWatcher() {
-        String spKey = LauncherAppState.getSharedPreferencesKey();
-        SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE);
-        boolean show = sp.getBoolean(SHOW_WEIGHT_WATCHER, true);
-
-        show = !show;
-
-        SharedPreferences.Editor editor = sp.edit();
-        editor.putBoolean(SHOW_WEIGHT_WATCHER, show);
-        editor.commit();
-
-        if (mWeightWatcher != null) {
-            mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE);
-        }
-    }
-
     public void bindAppsAdded(final ArrayList<Long> newScreens,
                               final ArrayList<ItemInfo> addNotAnimated,
                               final ArrayList<ItemInfo> addAnimated,
@@ -4012,6 +4001,21 @@
         mSynchronouslyBoundPages.add(page);
     }
 
+    @Override
+    public void executeOnNextDraw(ViewOnDrawExecutor executor) {
+        if (mPendingExecutor != null) {
+            mPendingExecutor.markCompleted();
+        }
+        mPendingExecutor = executor;
+        executor.attachTo(this);
+    }
+
+    public void clearPendingExecutor(ViewOnDrawExecutor executor) {
+        if (mPendingExecutor == executor) {
+            mPendingExecutor = null;
+        }
+    }
+
     /**
      * Callback saying that there aren't any more items to bind.
      *
@@ -4570,6 +4574,18 @@
                 UserHandleCompat.myUserHandle());
     }
 
+    /**
+     * Generates a dummy AppInfo for us to use to calculate BubbleTextView sizes.
+     */
+    public AppInfo createDummyAppInfo() {
+        Intent intent = new Intent();
+        intent.setComponent(new ComponentName(this, Launcher.class));
+        PackageManager pm = getPackageManager();
+        ResolveInfo info = pm.resolveActivity(intent, 0);
+        return new AppInfo(this, LauncherActivityInfoCompat.fromResolveInfo(info, this),
+                UserHandleCompat.myUserHandle(), mIconCache);
+    }
+
     // TODO: This method should be a part of LauncherSearchCallback
     public void startDrag(View dragView, ItemInfo dragInfo, DragSource source) {
         dragView.setTag(dragInfo);
@@ -4723,7 +4739,7 @@
                     }
                     return null;
                 }
-            }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
+            }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
         }
     }
 }
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 5a7fadb..3b207a9 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PackageInstallerCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
 
 import java.lang.ref.WeakReference;
@@ -78,8 +79,8 @@
 
         Log.v(Launcher.TAG, "LauncherAppState inited");
 
-        if (sContext.getResources().getBoolean(R.bool.debug_memory_enabled)) {
-            MemoryTracker.startTrackingMe(sContext, "L");
+        if (TestingUtils.MEMORY_DUMP_ENABLED) {
+            TestingUtils.startTrackingMemory(sContext);
         }
 
         mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 323794f..27cfd63 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.ManagedProfileHeuristic;
 import com.android.launcher3.util.Thunk;
+import com.android.launcher3.util.ViewOnDrawExecutor;
 
 import java.lang.ref.WeakReference;
 import java.net.URISyntaxException;
@@ -79,6 +80,7 @@
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
 /**
  * Maintains in-memory state of the Launcher. It is expected that there should be only one
@@ -124,12 +126,6 @@
     @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
-    // draw (in Workspace) to initiate the binding of the remaining side pages.  Any time we start
-    // a normal load, we also clear this set of Runnables.
-    static final ArrayList<Runnable> mDeferredBindRunnables = new ArrayList<Runnable>();
-
     /**
      * Set of runnables to be called on the background thread after the workspace binding
      * is complete.
@@ -171,6 +167,9 @@
     // sBgWidgetProviders is the set of widget providers including custom internal widgets
     public static HashMap<ComponentKey, LauncherAppWidgetProviderInfo> sBgWidgetProviders;
 
+    // sBgShortcutProviders is the set of custom shortcut providers
+    public static List<ResolveInfo> sBgShortcutProviders;
+
     // sPendingPackages is a set of packages which could be on sdcard and are not available yet
     static final HashMap<UserHandleCompat, HashSet<String>> sPendingPackages =
             new HashMap<UserHandleCompat, HashSet<String>>();
@@ -185,6 +184,7 @@
     public interface Callbacks {
         public boolean setLoadOnResume();
         public int getCurrentWorkspaceScreen();
+        public void clearPendingBinds();
         public void startBinding();
         public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end,
                               boolean forceAnimateIcons);
@@ -209,6 +209,7 @@
         public void bindSearchProviderChanged();
         public boolean isAllAppsButtonRank(int rank);
         public void onPageBoundSynchronously(int page);
+        public void executeOnNextDraw(ViewOnDrawExecutor executor);
         public void dumpLogsToLocalData();
     }
 
@@ -587,11 +588,6 @@
                     "main thread");
         }
 
-        // Clear any deferred bind runnables
-        synchronized (mDeferredBindRunnables) {
-            mDeferredBindRunnables.clear();
-        }
-
         // Remove any queued UI runnables
         mHandler.cancelAll();
         // Unbind all the workspace items
@@ -1339,14 +1335,16 @@
         // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
         InstallShortcutReceiver.enableInstallQueue();
         synchronized (mLock) {
-            // Clear any deferred bind-runnables from the synchronized load process
-            // We must do this before any loading/binding is scheduled below.
-            synchronized (mDeferredBindRunnables) {
-                mDeferredBindRunnables.clear();
-            }
-
             // Don't bother to start the thread if we know it's not going to do anything
             if (mCallbacks != null && mCallbacks.get() != null) {
+                final Callbacks oldCallbacks = mCallbacks.get();
+                // Clear any pending bind-runnables from the synchronized load process.
+                runOnMainThread(new Runnable() {
+                    public void run() {
+                        oldCallbacks.clearPendingBinds();
+                    }
+                });
+
                 // If there is already one running, tell it to stop.
                 stopLoaderLocked();
                 mLoaderTask = new LoaderTask(mApp.getContext(), loadFlags);
@@ -1361,21 +1359,6 @@
         }
     }
 
-    void bindRemainingSynchronousPages() {
-        // Post the remaining side pages to be loaded
-        if (!mDeferredBindRunnables.isEmpty()) {
-            Runnable[] deferredBindRunnables = null;
-            synchronized (mDeferredBindRunnables) {
-                deferredBindRunnables = mDeferredBindRunnables.toArray(
-                        new Runnable[mDeferredBindRunnables.size()]);
-                mDeferredBindRunnables.clear();
-            }
-            for (final Runnable r : deferredBindRunnables) {
-                mHandler.post(r);
-            }
-        }
-    }
-
     public void stopLoader() {
         synchronized (mLock) {
             if (mLoaderTask != null) {
@@ -2379,7 +2362,7 @@
             Collections.sort(allWorkspaceItems, new Comparator<ItemInfo>() {
                 @Override
                 public int compare(ItemInfo lhs, ItemInfo rhs) {
-                    return (int) (lhs.container - rhs.container);
+                    return Long.compare(lhs.container, rhs.container);
                 }
             });
             for (ItemInfo info : allWorkspaceItems) {
@@ -2501,9 +2484,7 @@
                 final ArrayList<ItemInfo> workspaceItems,
                 final ArrayList<LauncherAppWidgetInfo> appWidgets,
                 final LongArrayMap<FolderInfo> folders,
-                ArrayList<Runnable> deferredBindRunnables) {
-
-            final boolean postOnMainThread = (deferredBindRunnables != null);
+                final Executor executor) {
 
             // Bind the workspace items
             int N = workspaceItems.size();
@@ -2520,13 +2501,7 @@
                         }
                     }
                 };
-                if (postOnMainThread) {
-                    synchronized (deferredBindRunnables) {
-                        deferredBindRunnables.add(r);
-                    }
-                } else {
-                    runOnMainThread(r);
-                }
+                executor.execute(r);
             }
 
             // Bind the folders
@@ -2539,13 +2514,7 @@
                         }
                     }
                 };
-                if (postOnMainThread) {
-                    synchronized (deferredBindRunnables) {
-                        deferredBindRunnables.add(r);
-                    }
-                } else {
-                    runOnMainThread(r);
-                }
+                executor.execute(r);
             }
 
             // Bind the widgets, one at a time
@@ -2560,11 +2529,7 @@
                         }
                     }
                 };
-                if (postOnMainThread) {
-                    deferredBindRunnables.add(r);
-                } else {
-                    runOnMainThread(r);
-                }
+                executor.execute(r);
             }
         }
 
@@ -2642,6 +2607,7 @@
                 public void run() {
                     Callbacks callbacks = tryGetCallbacks(oldCallbacks);
                     if (callbacks != null) {
+                        callbacks.clearPendingBinds();
                         callbacks.startBinding();
                     }
                 }
@@ -2650,28 +2616,20 @@
 
             bindWorkspaceScreens(oldCallbacks, orderedScreenIds);
 
-            // Load items on the current page
+            Executor mainExecutor = new DeferredMainThreadExecutor();
+            // Load items on the current page.
             bindWorkspaceItems(oldCallbacks, currentWorkspaceItems, currentAppWidgets,
-                    currentFolders, null);
-            if (isLoadingSynchronously) {
-                r = new Runnable() {
-                    public void run() {
-                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
-                        if (callbacks != null && currentScreen != PagedView.INVALID_RESTORE_PAGE) {
-                            callbacks.onPageBoundSynchronously(currentScreen);
-                        }
-                    }
-                };
-                runOnMainThread(r);
-            }
+                    currentFolders, mainExecutor);
 
-            // Load all the remaining pages (if we are loading synchronously, we want to defer this
-            // work until after the first render)
-            synchronized (mDeferredBindRunnables) {
-                mDeferredBindRunnables.clear();
-            }
-            bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets, otherFolders,
-                    (isLoadingSynchronously ? mDeferredBindRunnables : null));
+            // In case of isLoadingSynchronously, only bind the first screen, and defer binding the
+            // remaining screens after first onDraw is called. This ensures that the first screen
+            // is immediately visible (eg. during rotation)
+            // In case of !isLoadingSynchronously, bind all pages one after other.
+            final Executor deferredExecutor = isLoadingSynchronously ?
+                    new ViewOnDrawExecutor(mHandler) : mainExecutor;
+
+            bindWorkspaceItems(oldCallbacks, otherWorkspaceItems, otherAppWidgets,
+                    otherFolders, deferredExecutor);
 
             // Tell the workspace that we're done binding items
             r = new Runnable() {
@@ -2701,11 +2659,23 @@
 
                 }
             };
+            deferredExecutor.execute(r);
+
             if (isLoadingSynchronously) {
-                synchronized (mDeferredBindRunnables) {
-                    mDeferredBindRunnables.add(r);
-                }
-            } else {
+                r = new Runnable() {
+                    public void run() {
+                        Callbacks callbacks = tryGetCallbacks(oldCallbacks);
+                        if (callbacks != null) {
+                            // We are loading synchronously, which means, some of the pages will be
+                            // bound after first draw. Inform the callbacks that page binding is
+                            // not complete, and schedule the remaining pages.
+                            if (currentScreen != PagedView.INVALID_RESTORE_PAGE) {
+                                callbacks.onPageBoundSynchronously(currentScreen);
+                            }
+                            callbacks.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
+                        }
+                    }
+                };
                 runOnMainThread(r);
             }
         }
@@ -3315,10 +3285,14 @@
                     PackageManager pm = context.getPackageManager();
                     for (String pkg : mPackages) {
                         try {
-                            needToRefresh |= !pm.queryBroadcastReceivers(
+                            List<ResolveInfo> widgets = pm.queryBroadcastReceivers(
                                     new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
-                                            .setPackage(pkg), 0).isEmpty();
+                                            .setPackage(pkg), 0);
+                            needToRefresh |= widgets != null && !widgets.isEmpty();
                         } catch (RuntimeException e) {
+                            if (ProviderConfig.IS_DOGFOOD_BUILD) {
+                                throw e;
+                            }
                             // Ignore the crash. We can live with a state widget list.
                             Log.e(TAG, "PM call failed for " + pkg, e);
                         }
@@ -3373,8 +3347,9 @@
                 return results;
             }
         } catch (Exception e) {
-            if (e.getCause() instanceof TransactionTooLargeException ||
-                    e.getCause() instanceof DeadObjectException) {
+            if (!ProviderConfig.IS_DOGFOOD_BUILD &&
+                    (e.getCause() instanceof TransactionTooLargeException ||
+                    e.getCause() instanceof DeadObjectException)) {
                 // the returned value may be incomplete and will not be refreshed until the next
                 // time Launcher starts.
                 // TODO: after figuring out a repro step, introduce a dirty bit to check when
@@ -3434,8 +3409,29 @@
         PackageManager packageManager = mApp.getContext().getPackageManager();
         final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>();
         widgetsAndShortcuts.addAll(getWidgetProviders(mApp.getContext(), refresh));
-        Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
-        widgetsAndShortcuts.addAll(packageManager.queryIntentActivities(shortcutsIntent, 0));
+
+        // Update shortcut providers
+        synchronized (sBgLock) {
+            try {
+                Intent shortcutsIntent = new Intent(Intent.ACTION_CREATE_SHORTCUT);
+                List<ResolveInfo> providers = packageManager.queryIntentActivities(shortcutsIntent, 0);
+                sBgShortcutProviders = providers;
+            } catch (RuntimeException e) {
+                if (!ProviderConfig.IS_DOGFOOD_BUILD &&
+                        (e.getCause() instanceof TransactionTooLargeException ||
+                                e.getCause() instanceof DeadObjectException)) {
+                    /**
+                     * Ignore exception and use the cached list if available.
+                     * Refer to {@link #getWidgetProviders(Context, boolean}} for more info.
+                     */
+                } else {
+                    throw e;
+                }
+            }
+            if (sBgShortcutProviders != null) {
+                widgetsAndShortcuts.addAll(sBgShortcutProviders);
+            }
+        }
         mBgWidgetsModel.setWidgetsAndShortcuts(widgetsAndShortcuts);
     }
 
@@ -3737,6 +3733,14 @@
         }
     }
 
+    @Thunk class DeferredMainThreadExecutor implements Executor {
+
+        @Override
+        public void execute(Runnable command) {
+            runOnMainThread(command);
+        }
+    }
+
     /**
      * @return the looper for the worker thread which can be used to start background tasks.
      */
diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
index d3af19a..8778805 100644
--- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java
+++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
@@ -805,26 +805,27 @@
                 toWorkspaceState.getSearchDropTargetBarState();
 
         if (overlaySearchBar != null) {
-            if ((toWorkspaceState == Workspace.State.NORMAL) &&
-                    (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN)) {
-                // If we are transitioning from the overlay to the workspace, then show the
-                // workspace search bar immediately and let the overlay search bar fade out on top
-                mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0);
-            } else if (fromWorkspaceState == Workspace.State.NORMAL) {
-                // If we are transitioning from the workspace to the overlay, then keep the
-                // workspace search bar visible until the overlay search bar fades in on top
-                animation.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0);
-                    }
-                });
-            } else {
-                // Otherwise, then just animate the workspace search bar normally
-                mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration);
+            if (mLauncher.launcherCallbacksProvidesSearch()) {
+                if ((toWorkspaceState == Workspace.State.NORMAL) &&
+                        (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN)) {
+                    // If we are transitioning from the overlay to the workspace, then show the
+                    // workspace search bar immediately and let the overlay search bar fade out on
+                    // top
+                    mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0);
+                    return;
+                } else if (fromWorkspaceState == Workspace.State.NORMAL) {
+                    // If we are transitioning from the workspace to the overlay, then keep the
+                    // workspace search bar visible until the overlay search bar fades in on top
+                    animation.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, 0);
+                        }
+                    });
+                    return;
+                }
             }
-        } else {
-            // If there is no overlay search bar, then just animate the workspace search bar
+            // Fallback to the default search bar animation otherwise
             mLauncher.getSearchDropTargetBar().animateToState(toSearchBarState, duration);
         }
     }
diff --git a/src/com/android/launcher3/PendingAppWidgetHostView.java b/src/com/android/launcher3/PendingAppWidgetHostView.java
index 40eadab..1d76945 100644
--- a/src/com/android/launcher3/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/PendingAppWidgetHostView.java
@@ -133,7 +133,7 @@
             //   3) Setup icon in the center and app icon in the top right corner.
             if (mDisabledForSafeMode) {
                 FastBitmapDrawable disabledIcon = mLauncher.createIconDrawable(mIcon);
-                disabledIcon.setGhostModeEnabled(true);
+                disabledIcon.setState(FastBitmapDrawable.State.DISABLED);
                 mCenterDrawable = disabledIcon;
                 mSettingIconDrawable = null;
             } else if (isReadyForClickSetup()) {
diff --git a/src/com/android/launcher3/PreloadIconDrawable.java b/src/com/android/launcher3/PreloadIconDrawable.java
index 45e4b2c..908c8b9 100644
--- a/src/com/android/launcher3/PreloadIconDrawable.java
+++ b/src/com/android/launcher3/PreloadIconDrawable.java
@@ -179,7 +179,8 @@
             mPaint.setColor(getIndicatorColor());
         }
         if (mIcon instanceof FastBitmapDrawable) {
-            ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0);
+            ((FastBitmapDrawable) mIcon).setState(level <= 0 ?
+                    FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL);
         }
 
         invalidateSelf();
diff --git a/src/com/android/launcher3/ToggleWeightWatcher.java b/src/com/android/launcher3/ToggleWeightWatcher.java
deleted file mode 100644
index 33701a2..0000000
--- a/src/com/android/launcher3/ToggleWeightWatcher.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.android.launcher3;
-
-import android.app.Activity;
-
-public class ToggleWeightWatcher extends Activity {
-
-}
diff --git a/src/com/android/launcher3/UninstallDropTarget.java b/src/com/android/launcher3/UninstallDropTarget.java
index b69c79d..7388161 100644
--- a/src/com/android/launcher3/UninstallDropTarget.java
+++ b/src/com/android/launcher3/UninstallDropTarget.java
@@ -87,9 +87,12 @@
             final Runnable checkIfUninstallWasSuccess = new Runnable() {
                 @Override
                 public void run() {
-                    String packageName = componentInfo.first.getPackageName();
-                    boolean uninstallSuccessful = !AllAppsList.packageHasActivities(
-                            getContext(), packageName, user);
+                    boolean uninstallSuccessful = false;
+                    if (componentInfo != null) {
+                        String packageName = componentInfo.first.getPackageName();
+                        uninstallSuccessful = !AllAppsList.packageHasActivities(
+                                getContext(), packageName, user);
+                    }
                     sendUninstallResult(d.dragSource, uninstallSuccessful);
                 }
             };
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 4e93684..082800c 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -47,7 +47,10 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Process;
+import android.text.Spannable;
+import android.text.SpannableString;
 import android.text.TextUtils;
+import android.text.style.TtsSpan;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.Pair;
@@ -64,6 +67,10 @@
 import java.util.ArrayList;
 import java.util.Locale;
 import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -108,6 +115,18 @@
     public static final boolean ATLEAST_JB_MR2 =
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
 
+    // These values are same as that in {@link AsyncTask}.
+    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
+    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
+    private static final int KEEP_ALIVE = 1;
+    /**
+     * An {@link Executor} to be used with async task with no limit on the queue size.
+     */
+    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
+            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
+            TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
     // To turn on these properties, type
     // adb shell setprop log.tag.PROPERTY_NAME [VERBOSE | SUPPRESS]
     private static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate";
@@ -206,7 +225,7 @@
                 // Ensure the bitmap has a density.
                 BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
                 Bitmap bitmap = bitmapDrawable.getBitmap();
-                if (bitmap.getDensity() == Bitmap.DENSITY_NONE) {
+                if (bitmap != null && bitmap.getDensity() == Bitmap.DENSITY_NONE) {
                     bitmapDrawable.setTargetDensity(context.getResources().getDisplayMetrics());
                 }
             }
@@ -713,7 +732,6 @@
         return String.format(Locale.ENGLISH, "%s IN (%s)", columnName, TextUtils.join(", ", values));
     }
 
-    @SuppressWarnings({"unchecked", "rawtypes"})
     public static boolean isBootCompleted() {
         try {
             Class clazz = Class.forName("android.os.SystemProperties");
@@ -735,4 +753,22 @@
     public static int boundInRange(int value, int lowerBound, int upperBound) {
         return Math.max(lowerBound, Math.min(value, upperBound));
     }
+
+    /**
+     * Wraps a message with a TTS span, so that a different message is spoken than
+     * what is getting displayed.
+     * @param msg original message
+     * @param ttsMsg message to be spoken
+     */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public static CharSequence wrapForTts(CharSequence msg, String ttsMsg) {
+        if (Utilities.ATLEAST_LOLLIPOP) {
+            SpannableString spanned = new SpannableString(msg);
+            spanned.setSpan(new TtsSpan.TextBuilder(ttsMsg).build(),
+                    0, spanned.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+            return spanned;
+        } else {
+            return msg;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index 3460555..603b072 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -96,7 +96,7 @@
         WidgetCacheKey key = getObjectKey(o, size);
 
         PreviewLoadTask task = new PreviewLoadTask(key, o, previewWidth, previewHeight, caller);
-        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        task.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
         return new PreviewLoadRequest(task);
     }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 9c726b1..0db5f03 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -64,6 +64,7 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.AccessibilityDragSource;
 import com.android.launcher3.accessibility.OverviewScreenAccessibilityDelegate;
+import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.config.ProviderConfig;
 import com.android.launcher3.dragndrop.DragController;
@@ -278,13 +279,6 @@
 
     private AccessibilityDelegate mPagesAccessibilityDelegate;
 
-    private final Runnable mBindPages = new Runnable() {
-        @Override
-        public void run() {
-            mLauncher.getModel().bindRemainingSynchronousPages();
-        }
-    };
-
     /**
      * Used to inflate the Workspace from XML.
      *
@@ -1318,7 +1312,7 @@
                 }
                 return null;
             }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
+        }.executeOnExecutor(Utilities.THREAD_POOL_EXECUTOR);
     }
 
     protected void snapToPage(int whichPage, Runnable r) {
@@ -1603,7 +1597,7 @@
     }
 
     public boolean isOnOrMovingToCustomContent() {
-        return hasCustomContent() && getNextPage() == 0;
+        return hasCustomContent() && getNextPage() == 0 && mRestorePage == INVALID_RESTORE_PAGE;
     }
 
     private void updateStateForCustomContent(int screenCenter) {
@@ -1720,14 +1714,6 @@
     }
 
     @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-
-        // Call back to LauncherModel to finish binding after the first draw
-        post(mBindPages);
-    }
-
-    @Override
     protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
         if (!mLauncher.isAppsViewVisible()) {
             final Folder openFolder = getOpenFolder();
@@ -2052,7 +2038,7 @@
             }
             setImportantForAccessibility((mState == State.NORMAL || mState == State.OVERVIEW)
                     ? IMPORTANT_FOR_ACCESSIBILITY_AUTO
-                            : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+                    : IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
         } else {
             int accessible = mState == State.NORMAL ?
                     IMPORTANT_FOR_ACCESSIBILITY_AUTO :
@@ -2507,11 +2493,14 @@
         return true;
     }
 
-    boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell, float
-            distance, boolean considerTimeout) {
+    boolean willCreateUserFolder(ItemInfo info, CellLayout target, int[] targetCell,
+            float distance, boolean considerTimeout) {
         if (distance > mMaxDistanceForFolderCreation) return false;
         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
+        return willCreateUserFolder(info, dropOverView, considerTimeout);
+    }
 
+    boolean willCreateUserFolder(ItemInfo info, View dropOverView, boolean considerTimeout) {
         if (dropOverView != null) {
             CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
             if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) {
@@ -2531,7 +2520,7 @@
         boolean aboveShortcut = (dropOverView.getTag() instanceof ShortcutInfo);
         boolean willBecomeShortcut =
                 (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION ||
-                info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
+                        info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
 
         return (aboveShortcut && willBecomeShortcut);
     }
@@ -2540,7 +2529,10 @@
             float distance) {
         if (distance > mMaxDistanceForFolderCreation) return false;
         View dropOverView = target.getChildAt(targetCell[0], targetCell[1]);
+        return willAddToExistingUserFolder(dragInfo, dropOverView);
 
+    }
+    boolean willAddToExistingUserFolder(ItemInfo dragInfo, View dropOverView) {
         if (dropOverView != null) {
             CellLayout.LayoutParams lp = (CellLayout.LayoutParams) dropOverView.getLayoutParams();
             if (lp.useTmpCoords && (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.tmpCellY)) {
@@ -3178,8 +3170,6 @@
                 mapPointFromSelfToChild(mDragTargetLayout, mDragViewVisualCenter);
             }
 
-            ItemInfo info = d.dragInfo;
-
             int minSpanX = item.spanX;
             int minSpanY = item.spanY;
             if (item.minSpanX > 0 && item.minSpanY > 0) {
@@ -3198,11 +3188,7 @@
             float targetCellDistance = mDragTargetLayout.getDistanceFromCell(
                     mDragViewVisualCenter[0], mDragViewVisualCenter[1], mTargetCell);
 
-            final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0],
-                    mTargetCell[1]);
-
-            manageFolderFeedback(info, mDragTargetLayout, mTargetCell,
-                    targetCellDistance, dragOverView, d.accessibleDrag);
+            manageFolderFeedback(mDragTargetLayout, mTargetCell, targetCellDistance, d);
 
             boolean nearestDropOccupied = mDragTargetLayout.isNearestDropLocationOccupied((int)
                     mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1], item.spanX,
@@ -3211,8 +3197,7 @@
             if (!nearestDropOccupied) {
                 mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
                         (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
-                        mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false,
-                        d.dragView.getDragVisualizeOffset(), d.dragView.getDragRegion());
+                        mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d);
             } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                     && !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
                     mLastReorderY != reorderY)) {
@@ -3225,7 +3210,7 @@
                 // Otherwise, if we aren't adding to or creating a folder and there's no pending
                 // reorder, then we schedule a reorder
                 ReorderAlarmListener listener = new ReorderAlarmListener(mDragViewVisualCenter,
-                        minSpanX, minSpanY, item.spanX, item.spanY, d.dragView, child);
+                        minSpanX, minSpanY, item.spanX, item.spanY, d, child);
                 mReorderAlarm.setOnAlarmListener(listener);
                 mReorderAlarm.setAlarm(REORDER_TIMEOUT);
             }
@@ -3239,28 +3224,34 @@
         }
     }
 
-    private void manageFolderFeedback(ItemInfo info, CellLayout targetLayout,
-            int[] targetCell, float distance, View dragOverView, boolean accessibleDrag) {
-        boolean userFolderPending = willCreateUserFolder(info, targetLayout, targetCell, distance,
-                false);
+    private void manageFolderFeedback(CellLayout targetLayout,
+            int[] targetCell, float distance, DragObject dragObject) {
+        if (distance > mMaxDistanceForFolderCreation) return;
+
+        final View dragOverView = mDragTargetLayout.getChildAt(mTargetCell[0], mTargetCell[1]);
+        ItemInfo info = dragObject.dragInfo;
+        boolean userFolderPending = willCreateUserFolder(info, dragOverView, false);
         if (mDragMode == DRAG_MODE_NONE && userFolderPending &&
                 !mFolderCreationAlarm.alarmPending()) {
 
             FolderCreationAlarmListener listener = new
                     FolderCreationAlarmListener(targetLayout, targetCell[0], targetCell[1]);
 
-            if (!accessibleDrag) {
+            if (!dragObject.accessibleDrag) {
                 mFolderCreationAlarm.setOnAlarmListener(listener);
                 mFolderCreationAlarm.setAlarm(FOLDER_CREATION_TIMEOUT);
             } else {
                 listener.onAlarm(mFolderCreationAlarm);
             }
+
+            if (dragObject.stateAnnouncer != null) {
+                dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
+                        .getDescriptionForDropOver(dragOverView, getContext()));
+            }
             return;
         }
 
-        boolean willAddToFolder =
-                willAddToExistingUserFolder(info, targetLayout, targetCell, distance);
-
+        boolean willAddToFolder = willAddToExistingUserFolder(info, dragOverView);
         if (willAddToFolder && mDragMode == DRAG_MODE_NONE) {
             mDragOverFolderIcon = ((FolderIcon) dragOverView);
             mDragOverFolderIcon.onDragEnter(info);
@@ -3268,6 +3259,11 @@
                 targetLayout.clearDragOutlines();
             }
             setDragMode(DRAG_MODE_ADD_TO_FOLDER);
+
+            if (dragObject.stateAnnouncer != null) {
+                dragObject.stateAnnouncer.announce(WorkspaceAccessibilityHelper
+                        .getDescriptionForDropOver(dragOverView, getContext()));
+            }
             return;
         }
 
@@ -3277,8 +3273,6 @@
         if (mDragMode == DRAG_MODE_CREATE_FOLDER && !userFolderPending) {
             setDragMode(DRAG_MODE_NONE);
         }
-
-        return;
     }
 
     class FolderCreationAlarmListener implements OnAlarmListener {
@@ -3310,18 +3304,18 @@
     class ReorderAlarmListener implements OnAlarmListener {
         float[] dragViewCenter;
         int minSpanX, minSpanY, spanX, spanY;
-        DragView dragView;
+        DragObject dragObject;
         View child;
 
         public ReorderAlarmListener(float[] dragViewCenter, int minSpanX, int minSpanY, int spanX,
-                int spanY, DragView dragView, View child) {
+                int spanY, DragObject dragObject, View child) {
             this.dragViewCenter = dragViewCenter;
             this.minSpanX = minSpanX;
             this.minSpanY = minSpanY;
             this.spanX = spanX;
             this.spanY = spanY;
             this.child = child;
-            this.dragView = dragView;
+            this.dragObject = dragObject;
         }
 
         public void onAlarm(Alarm alarm) {
@@ -3345,8 +3339,7 @@
             boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
             mDragTargetLayout.visualizeDropLocation(child, mDragOutline,
                 (int) mDragViewVisualCenter[0], (int) mDragViewVisualCenter[1],
-                mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize,
-                dragView.getDragVisualizeOffset(), dragView.getDragRegion());
+                mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject);
         }
     }
 
@@ -4406,8 +4399,16 @@
 
     private String getPageDescription(int page) {
         int delta = numCustomPages();
+        int nScreens = getChildCount() - delta;
+        int extraScreenId = mScreenOrder.indexOf(EXTRA_EMPTY_SCREEN_ID);
+        if (extraScreenId >= 0 && nScreens > 1) {
+            if (page == extraScreenId) {
+                return getContext().getString(R.string.workspace_new_page);
+            }
+            nScreens--;
+        }
         return getContext().getString(R.string.workspace_scroll_format,
-                page + 1 - delta, getChildCount() - delta);
+                page + 1 - delta, nScreens);
     }
 
     public void getLocationInDragLayer(int[] loc) {
diff --git a/src/com/android/launcher3/accessibility/DragViewStateAnnouncer.java b/src/com/android/launcher3/accessibility/DragViewStateAnnouncer.java
new file mode 100644
index 0000000..8ff82dd
--- /dev/null
+++ b/src/com/android/launcher3/accessibility/DragViewStateAnnouncer.java
@@ -0,0 +1,61 @@
+/*
+ * 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.accessibility;
+
+import android.content.Context;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * Periodically sends accessibility events to announce ongoing state changed. Based on the
+ * implementation in ProgressBar.
+ */
+public class DragViewStateAnnouncer implements Runnable {
+
+    private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
+
+    private final View mTargetView;
+
+    private DragViewStateAnnouncer(View view) {
+        mTargetView = view;
+    }
+
+    public void announce(CharSequence msg) {
+        mTargetView.setContentDescription(msg);
+        mTargetView.removeCallbacks(this);
+        mTargetView.postDelayed(this, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
+    }
+
+    public void cancel() {
+        mTargetView.removeCallbacks(this);
+    }
+
+    @Override
+    public void run() {
+        mTargetView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+    }
+
+    public static DragViewStateAnnouncer createFor(View v) {
+        if (((AccessibilityManager) v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE))
+                .isEnabled()) {
+            return new DragViewStateAnnouncer(v);
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 9479685..f8bf5a8 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -134,11 +134,8 @@
 
     public boolean performAction(final View host, final ItemInfo item, int action) {
         if (action == REMOVE) {
-            if (DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host)) {
-                announceConfirmation(R.string.item_removed);
-                return true;
-            }
-            return false;
+            DeleteDropTarget.removeWorkspaceOrFolderItem(mLauncher, item, host);
+            return true;
         } else if (action == INFO) {
             InfoDropTarget.startDetailsActivityForInfo(item, mLauncher);
             return true;
diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
index 80ddc13..73b824b 100644
--- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
+++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.accessibility;
 
+import android.content.Context;
 import android.text.TextUtils;
 import android.view.View;
 
@@ -140,26 +141,30 @@
                 return mContext.getString(R.string.move_to_empty_cell, y + 1, x + 1);
             }
         } else {
-            ItemInfo info = (ItemInfo) child.getTag();
-            if (info instanceof ShortcutInfo) {
-                return mContext.getString(R.string.create_folder_with, info.title);
-            } else if (info instanceof FolderInfo) {
-                if (TextUtils.isEmpty(info.title)) {
-                    // Find the first item in the folder.
-                    FolderInfo folder = (FolderInfo) info;
-                    ShortcutInfo firstItem = null;
-                    for (ShortcutInfo shortcut : folder.contents) {
-                        if (firstItem == null || firstItem.rank > shortcut.rank) {
-                            firstItem = shortcut;
-                        }
-                    }
+            return getDescriptionForDropOver(child, mContext);
+        }
+    }
 
-                    if (firstItem != null) {
-                        return mContext.getString(R.string.add_to_folder_with_app, firstItem.title);
+    public static String getDescriptionForDropOver(View overChild, Context context) {
+        ItemInfo info = (ItemInfo) overChild.getTag();
+        if (info instanceof ShortcutInfo) {
+            return context.getString(R.string.create_folder_with, info.title);
+        } else if (info instanceof FolderInfo) {
+            if (TextUtils.isEmpty(info.title)) {
+                // Find the first item in the folder.
+                FolderInfo folder = (FolderInfo) info;
+                ShortcutInfo firstItem = null;
+                for (ShortcutInfo shortcut : folder.contents) {
+                    if (firstItem == null || firstItem.rank > shortcut.rank) {
+                        firstItem = shortcut;
                     }
                 }
-                return mContext.getString(R.string.add_to_folder, info.title);
+
+                if (firstItem != null) {
+                    return context.getString(R.string.add_to_folder_with_app, firstItem.title);
+                }
             }
+            return context.getString(R.string.add_to_folder, info.title);
         }
         return "";
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java
index 117aca9..dafa73f 100644
--- a/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java
+++ b/src/com/android/launcher3/allapps/AllAppsBackgroundDrawable.java
@@ -35,6 +35,7 @@
     private float mXPercent;
     private float mYPercent;
     private int mGravity;
+    private int mAlpha;
 
     /**
      * @param gravity If one of the Gravity center values, the x and y offset will take the width
@@ -50,10 +51,11 @@
 
     public void setAlpha(int alpha) {
         mImage.setAlpha(alpha);
+        mAlpha = alpha;
     }
 
     public int getAlpha() {
-        return mImage.getAlpha();
+        return mAlpha;
     }
 
     public void updateBounds(Rect bounds) {
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 979a733..c26463e 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -28,6 +28,7 @@
 import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -35,6 +36,7 @@
 import android.widget.LinearLayout;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseContainerView;
+import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
 import com.android.launcher3.DeleteDropTarget;
 import com.android.launcher3.DeviceProfile;
@@ -332,6 +334,18 @@
             mAppsRecyclerView.addItemDecoration(mItemDecoration);
         }
 
+        // Precalculate the prediction icon and normal icon sizes
+        LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+        BubbleTextView icon = (BubbleTextView) layoutInflater.inflate(R.layout.all_apps_icon, this, false);
+        icon.applyFromApplicationInfo(mLauncher.createDummyAppInfo());
+        icon.measure(MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST));
+        BubbleTextView predIcon = (BubbleTextView) layoutInflater.inflate(R.layout.all_apps_prediction_bar_icon, this, false);
+        predIcon.applyFromApplicationInfo(mLauncher.createDummyAppInfo());
+        predIcon.measure(MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST));
+        mAppsRecyclerView.setPremeasuredIconHeights(predIcon.getMeasuredHeight(), icon.getMeasuredHeight());
+
         updateBackgroundAndPaddings();
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
new file mode 100644
index 0000000..1045342
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AllAppsFastScrollHelper.java
@@ -0,0 +1,228 @@
+/*
+ * 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.allapps;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.android.launcher3.BaseRecyclerView;
+import com.android.launcher3.BaseRecyclerViewFastScrollBar;
+import com.android.launcher3.FastBitmapDrawable;
+import com.android.launcher3.util.Thunk;
+
+import java.util.HashSet;
+
+public class AllAppsFastScrollHelper implements AllAppsGridAdapter.BindViewCallback {
+
+    private static final int INITIAL_TOUCH_SETTLING_DURATION = 300;
+    private static final int REPEAT_TOUCH_SETTLING_DURATION = 200;
+    private static final float FAST_SCROLL_TOUCH_VELOCITY_BARRIER = 1900f;
+
+    private AllAppsRecyclerView mRv;
+    private AlphabeticalAppsList mApps;
+
+    // Keeps track of the current and targetted fast scroll section (the section to scroll to after
+    // the initial delay)
+    int mTargetFastScrollPosition = -1;
+    @Thunk String mCurrentFastScrollSection;
+    @Thunk String mTargetFastScrollSection;
+
+    // The settled states affect the delay before the fast scroll animation is applied
+    private boolean mHasFastScrollTouchSettled;
+    private boolean mHasFastScrollTouchSettledAtLeastOnce;
+
+    // Set of all views animated during fast scroll.  We keep track of these ourselves since there
+    // is no way to reset a view once it gets scrapped or recycled without other hacks
+    private HashSet<BaseRecyclerViewFastScrollBar.FastScrollFocusableView> mTrackedFastScrollViews =
+            new HashSet<>();
+
+    // Smooth fast-scroll animation frames
+    @Thunk int mFastScrollFrameIndex;
+    @Thunk final int[] mFastScrollFrames = new int[10];
+
+    /**
+     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
+     * if necessary.
+     */
+    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
+                mRv.scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
+                mFastScrollFrameIndex++;
+                mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
+            }
+        }
+    };
+
+    /**
+     * This runnable updates the current fast scroll section to the target fastscroll section.
+     */
+    Runnable mFastScrollToTargetSectionRunnable = new Runnable() {
+        @Override
+        public void run() {
+            // Update to the target section
+            mCurrentFastScrollSection = mTargetFastScrollSection;
+            mHasFastScrollTouchSettled = true;
+            mHasFastScrollTouchSettledAtLeastOnce = true;
+            updateTrackedViewsFastScrollFocusState();
+        }
+    };
+
+    public AllAppsFastScrollHelper(AllAppsRecyclerView rv, AlphabeticalAppsList apps) {
+        mRv = rv;
+        mApps = apps;
+    }
+
+    public void onSetAdapter(AllAppsGridAdapter adapter) {
+        adapter.setBindViewCallback(this);
+    }
+
+    /**
+     * Smooth scrolls the recycler view to the given section.
+     *
+     * @return whether the fastscroller can scroll to the new section.
+     */
+    public boolean smoothScrollToSection(int scrollY, int availableScrollHeight,
+            AlphabeticalAppsList.FastScrollSectionInfo info) {
+        if (mTargetFastScrollPosition != info.fastScrollToItem.position) {
+            mTargetFastScrollPosition = info.fastScrollToItem.position;
+            smoothSnapToPosition(scrollY, availableScrollHeight, info);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
+     * ourselves and animating the scroll on the recycler view.
+     */
+    private void smoothSnapToPosition(int scrollY, int availableScrollHeight,
+            AlphabeticalAppsList.FastScrollSectionInfo info) {
+        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
+        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
+
+        trackAllChildViews();
+        if (mHasFastScrollTouchSettled) {
+            // In this case, the user has already settled once (and the fast scroll state has
+            // animated) and they are just fine-tuning their section from the last section, so
+            // we should make it feel fast and update immediately.
+            mCurrentFastScrollSection = info.sectionName;
+            mTargetFastScrollSection = null;
+            updateTrackedViewsFastScrollFocusState();
+        } else {
+            // Otherwise, the user has scrubbed really far, and we don't want to distract the user
+            // with the flashing fast scroll state change animation in addition to the fast scroll
+            // section popup, so reset the views to normal, and wait for the touch to settle again
+            // before animating the fast scroll state.
+            mCurrentFastScrollSection = null;
+            mTargetFastScrollSection = info.sectionName;
+            mHasFastScrollTouchSettled = false;
+            updateTrackedViewsFastScrollFocusState();
+
+            // Delay scrolling to a new section until after some duration.  If the user has been
+            // scrubbing a while and makes multiple big jumps, then reduce the time needed for the
+            // fast scroll to settle so it doesn't feel so long.
+            mRv.postDelayed(mFastScrollToTargetSectionRunnable,
+                    mHasFastScrollTouchSettledAtLeastOnce ?
+                            REPEAT_TOUCH_SETTLING_DURATION :
+                            INITIAL_TOUCH_SETTLING_DURATION);
+        }
+
+        // Calculate the full animation from the current scroll position to the final scroll
+        // position, and then run the animation for the duration.
+        int newScrollY = Math.min(availableScrollHeight,
+                mRv.getPaddingTop() + mRv.getTop(info.fastScrollToItem.rowIndex));
+        int numFrames = mFastScrollFrames.length;
+        for (int i = 0; i < numFrames; i++) {
+            // TODO(winsonc): We can interpolate this as well.
+            mFastScrollFrames[i] = (newScrollY - scrollY) / numFrames;
+        }
+        mFastScrollFrameIndex = 0;
+        mRv.postOnAnimation(mSmoothSnapNextFrameRunnable);
+    }
+
+    public void onFastScrollCompleted() {
+        // TODO(winsonc): Handle the case when the user scrolls and releases before the animation
+        //                runs
+
+        // Stop animating the fast scroll position and state
+        mRv.removeCallbacks(mSmoothSnapNextFrameRunnable);
+        mRv.removeCallbacks(mFastScrollToTargetSectionRunnable);
+
+        // Reset the tracking variables
+        mHasFastScrollTouchSettled = false;
+        mHasFastScrollTouchSettledAtLeastOnce = false;
+        mCurrentFastScrollSection = null;
+        mTargetFastScrollSection = null;
+        mTargetFastScrollPosition = -1;
+
+        updateTrackedViewsFastScrollFocusState();
+        mTrackedFastScrollViews.clear();
+    }
+
+    @Override
+    public void onBindView(AllAppsGridAdapter.ViewHolder holder) {
+        // Update newly bound views to the current fast scroll state if we are fast scrolling
+        if (mCurrentFastScrollSection != null || mTargetFastScrollSection != null) {
+            if (holder.mContent instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                BaseRecyclerViewFastScrollBar.FastScrollFocusableView v =
+                        (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) holder.mContent;
+                updateViewFastScrollFocusState(v, holder.getPosition(), false /* animated */);
+                mTrackedFastScrollViews.add(v);
+            }
+        }
+    }
+
+    /**
+     * Starts tracking all the recycler view's children which are FastScrollFocusableViews.
+     */
+    private void trackAllChildViews() {
+        int childCount = mRv.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View v = mRv.getChildAt(i);
+            if (v instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
+                mTrackedFastScrollViews.add((BaseRecyclerViewFastScrollBar.FastScrollFocusableView) v);
+            }
+        }
+    }
+
+    /**
+     * Updates the fast scroll focus on all the children.
+     */
+    private void updateTrackedViewsFastScrollFocusState() {
+        for (BaseRecyclerViewFastScrollBar.FastScrollFocusableView v : mTrackedFastScrollViews) {
+            RecyclerView.ViewHolder viewHolder = mRv.getChildViewHolder((View) v);
+            int pos = (viewHolder != null) ? viewHolder.getPosition() : -1;
+            updateViewFastScrollFocusState(v, pos, true);
+        }
+    }
+
+    /**
+     * Updates the fast scroll focus on all a given view.
+     */
+    private void updateViewFastScrollFocusState(BaseRecyclerViewFastScrollBar.FastScrollFocusableView v,
+                                                int pos, boolean animated) {
+        FastBitmapDrawable.State newState = FastBitmapDrawable.State.NORMAL;
+        if (mCurrentFastScrollSection != null && pos > -1) {
+            AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(pos);
+            newState = item.sectionName.equals(mCurrentFastScrollSection) ?
+                    FastBitmapDrawable.State.FAST_SCROLL_HIGHLIGHTED :
+                    FastBitmapDrawable.State.FAST_SCROLL_UNHIGHLIGHTED;
+        }
+        v.setFastScrollFocusState(newState, animated);
+    }
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index f885567..b7fa43d 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -69,6 +69,10 @@
     // The message to continue to a market search when there are no filtered results
     public static final int SEARCH_MARKET_VIEW_TYPE = 5;
 
+    public interface BindViewCallback {
+        public void onBindView(ViewHolder holder);
+    }
+
     /**
      * ViewHolder for each icon.
      */
@@ -328,6 +332,7 @@
     private View.OnTouchListener mTouchListener;
     private View.OnClickListener mIconClickListener;
     private View.OnLongClickListener mIconLongClickListener;
+    private BindViewCallback mBindViewCallback;
     @Thunk final Rect mBackgroundPadding = new Rect();
     @Thunk int mPredictionBarDividerOffset;
     @Thunk int mAppsPerRow;
@@ -424,6 +429,13 @@
     }
 
     /**
+     * Sets the callback for when views are bound.
+     */
+    public void setBindViewCallback(BindViewCallback cb) {
+        mBindViewCallback = cb;
+    }
+
+    /**
      * Notifies the adapter of the background padding so that it can draw things correctly in the
      * item decorator.
      */
@@ -528,6 +540,15 @@
                 }
                 break;
         }
+        if (mBindViewCallback != null) {
+            mBindViewCallback.onBindView(holder);
+        }
+    }
+
+    @Override
+    public boolean onFailedToRecycleView(ViewHolder holder) {
+        // Always recycle and we will reset the view when it is bound
+        return true;
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 10d10f1..48b9494 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -15,24 +15,20 @@
  */
 package com.android.launcher3.allapps;
 
-import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.View;
 
 import com.android.launcher3.BaseRecyclerView;
-import com.android.launcher3.BaseRecyclerViewFastScrollBar;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Stats;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Thunk;
 
 import java.util.List;
 
@@ -42,25 +38,17 @@
 public class AllAppsRecyclerView extends BaseRecyclerView
         implements Stats.LaunchSourceProvider {
 
-    private static final int FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON = 0;
-    private static final int FAST_SCROLL_MODE_FREE_SCROLL = 1;
-
-    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW = 0;
-    private static final int FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS = 1;
-
     private AlphabeticalAppsList mApps;
+    private AllAppsFastScrollHelper mFastScrollHelper;
+    private BaseRecyclerView.ScrollPositionState mScrollPosState =
+            new BaseRecyclerView.ScrollPositionState();
     private int mNumAppsPerRow;
 
-    @Thunk BaseRecyclerViewFastScrollBar.FastScrollFocusableView mLastFastScrollFocusedView;
-    @Thunk int mPrevFastScrollFocusedPosition;
-    @Thunk int mFastScrollFrameIndex;
-    @Thunk final int[] mFastScrollFrames = new int[10];
+    // The specific icon heights that we use to calculate scroll
+    private int mPredictionIconHeight;
+    private int mIconHeight;
 
-    private final int mFastScrollMode = FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON;
-    private final int mScrollBarMode = FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW;
-
-    private ScrollPositionState mScrollPosState = new ScrollPositionState();
-
+    // The empty-search result background
     private AllAppsBackgroundDrawable mEmptySearchBackground;
     private int mEmptySearchBackgroundTopOffset;
 
@@ -79,8 +67,8 @@
     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr);
-
         Resources res = getResources();
+        addOnItemTouchListener(this);
         mScrollbar.setDetachThumbOnFastScroll();
         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
                 R.dimen.all_apps_empty_search_bg_top_offset);
@@ -91,6 +79,7 @@
      */
     public void setApps(AlphabeticalAppsList apps) {
         mApps = apps;
+        mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     }
 
     /**
@@ -110,6 +99,14 @@
     }
 
     /**
+     * Sets the heights of the icons in this view (for scroll calculations).
+     */
+    public void setPremeasuredIconHeights(int predictionIconHeight, int iconHeight) {
+        mPredictionIconHeight = predictionIconHeight;
+        mIconHeight = iconHeight;
+    }
+
+    /**
      * Scrolls this recycler view to the top.
      */
     public void scrollToTop() {
@@ -126,6 +123,7 @@
      */
     @Override
     protected void dispatchDraw(Canvas canvas) {
+        // Clip to ensure that we don't draw the overscroll effect beyond the background bounds
         canvas.clipRect(mBackgroundPadding.left, mBackgroundPadding.top,
                 getWidth() - mBackgroundPadding.right,
                 getHeight() - mBackgroundPadding.bottom);
@@ -157,14 +155,6 @@
     }
 
     @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        // Bind event handlers
-        addOnItemTouchListener(this);
-    }
-
-    @Override
     public void fillInLaunchSourceData(Bundle sourceData) {
         sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
         if (mApps.hasFilter()) {
@@ -212,63 +202,31 @@
         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
                 mApps.getFastScrollerSections();
         AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
-        if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_ROW) {
-            for (int i = 1; i < fastScrollSections.size(); i++) {
-                AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
-                if (info.touchFraction > touchFraction) {
-                    break;
-                }
-                lastInfo = info;
+        for (int i = 1; i < fastScrollSections.size(); i++) {
+            AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
+            if (info.touchFraction > touchFraction) {
+                break;
             }
-        } else if (mScrollBarMode == FAST_SCROLL_BAR_MODE_DISTRIBUTE_BY_SECTIONS){
-            lastInfo = fastScrollSections.get((int) (touchFraction * (fastScrollSections.size() - 1)));
-        } else {
-            throw new RuntimeException("Unexpected scroll bar mode");
+            lastInfo = info;
         }
 
-        // Map the touch position back to the scroll of the recycler view
-        getCurScrollState(mScrollPosState);
-        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
-        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
-        if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
-            layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
-        }
-
-        if (mPrevFastScrollFocusedPosition != lastInfo.fastScrollToItem.position) {
-            mPrevFastScrollFocusedPosition = lastInfo.fastScrollToItem.position;
-
-            // Reset the last focused view
-            if (mLastFastScrollFocusedView != null) {
-                mLastFastScrollFocusedView.setFastScrollFocused(false, true);
-                mLastFastScrollFocusedView = null;
-            }
-
-            if (mFastScrollMode == FAST_SCROLL_MODE_JUMP_TO_FIRST_ICON) {
-                smoothSnapToPosition(mPrevFastScrollFocusedPosition, mScrollPosState);
-            } else if (mFastScrollMode == FAST_SCROLL_MODE_FREE_SCROLL) {
-                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
-                if (vh != null &&
-                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView) {
-                    mLastFastScrollFocusedView =
-                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
-                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
-                }
-            } else {
-                throw new RuntimeException("Unexpected fast scroll mode");
-            }
-        }
+        // Update the fast scroll
+        int scrollY = getScrollTop(mScrollPosState);
+        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
+        mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
         return lastInfo.sectionName;
     }
 
     @Override
     public void onFastScrollCompleted() {
         super.onFastScrollCompleted();
-        // Reset and clean up the last focused view
-        if (mLastFastScrollFocusedView != null) {
-            mLastFastScrollFocusedView.setFastScrollFocused(false, true);
-            mLastFastScrollFocusedView = null;
-        }
-        mPrevFastScrollFocusedPosition = -1;
+        mFastScrollHelper.onFastScrollCompleted();
+    }
+
+    @Override
+    public void setAdapter(Adapter adapter) {
+        super.setAdapter(adapter);
+        mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
     }
 
     /**
@@ -286,7 +244,7 @@
 
         // Find the index and height of the first visible row (all rows have the same height)
         int rowCount = mApps.getNumAppRows();
-        getCurScrollState(mScrollPosState);
+        getCurScrollState(mScrollPosState, -1);
         if (mScrollPosState.rowIndex < 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -294,7 +252,7 @@
 
         // Only show the scrollbar if there is height to be scrolled
         int availableScrollBarHeight = getAvailableScrollBarHeight();
-        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows(), mScrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(mApps.getNumAppRows());
         if (availableScrollHeight <= 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -303,8 +261,7 @@
         // Calculate the current scroll position, the scrollY of the recycler view accounts for the
         // view padding, while the scrollBarY is drawn right up to the background padding (ignoring
         // padding)
-        int scrollY = getPaddingTop() +
-                (mScrollPosState.rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
+        int scrollY = getScrollTop(mScrollPosState);
         int scrollBarY = mBackgroundPadding.top +
                 (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
 
@@ -355,58 +312,12 @@
     }
 
     /**
-     * This runnable runs a single frame of the smooth scroll animation and posts the next frame
-     * if necessary.
-     */
-    @Thunk Runnable mSmoothSnapNextFrameRunnable = new Runnable() {
-        @Override
-        public void run() {
-            if (mFastScrollFrameIndex < mFastScrollFrames.length) {
-                scrollBy(0, mFastScrollFrames[mFastScrollFrameIndex]);
-                mFastScrollFrameIndex++;
-                postOnAnimation(mSmoothSnapNextFrameRunnable);
-            } else {
-                // Animation completed, set the fast scroll state on the target view
-                final ViewHolder vh = findViewHolderForPosition(mPrevFastScrollFocusedPosition);
-                if (vh != null &&
-                        vh.itemView instanceof BaseRecyclerViewFastScrollBar.FastScrollFocusableView &&
-                        mLastFastScrollFocusedView != vh.itemView) {
-                    mLastFastScrollFocusedView =
-                            (BaseRecyclerViewFastScrollBar.FastScrollFocusableView) vh.itemView;
-                    mLastFastScrollFocusedView.setFastScrollFocused(true, true);
-                }
-            }
-        }
-    };
-
-    /**
-     * Smoothly snaps to a given position.  We do this manually by calculating the keyframes
-     * ourselves and animating the scroll on the recycler view.
-     */
-    private void smoothSnapToPosition(final int position, ScrollPositionState scrollPosState) {
-        removeCallbacks(mSmoothSnapNextFrameRunnable);
-
-        // Calculate the full animation from the current scroll position to the final scroll
-        // position, and then run the animation for the duration.
-        int curScrollY = getPaddingTop() +
-                (scrollPosState.rowIndex * scrollPosState.rowHeight) - scrollPosState.rowTopOffset;
-        int newScrollY = getScrollAtPosition(position, scrollPosState.rowHeight);
-        int numFrames = mFastScrollFrames.length;
-        for (int i = 0; i < numFrames; i++) {
-            // TODO(winsonc): We can interpolate this as well.
-            mFastScrollFrames[i] = (newScrollY - curScrollY) / numFrames;
-        }
-        mFastScrollFrameIndex = 0;
-        postOnAnimation(mSmoothSnapNextFrameRunnable);
-    }
-
-    /**
      * Returns the current scroll state of the apps rows.
      */
-    protected void getCurScrollState(ScrollPositionState stateOut) {
+    protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
         stateOut.rowIndex = -1;
         stateOut.rowTopOffset = -1;
-        stateOut.rowHeight = -1;
+        stateOut.itemPos = -1;
 
         // Return early if there are no items or we haven't been measured
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
@@ -420,15 +331,15 @@
             int position = getChildPosition(child);
             if (position != NO_POSITION) {
                 AlphabeticalAppsList.AdapterItem item = items.get(position);
-                if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
-                        item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
+                if ((item.viewType & viewTypeMask) != 0) {
                     stateOut.rowIndex = item.rowIndex;
                     stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
-                    stateOut.rowHeight = child.getHeight();
-                    break;
+                    stateOut.itemPos = position;
+                    return;
                 }
             }
         }
+        return;
     }
 
     @Override
@@ -438,18 +349,13 @@
         return !mApps.hasFilter();
     }
 
-    /**
-     * Returns the scrollY for the given position in the adapter.
-     */
-    private int getScrollAtPosition(int position, int rowHeight) {
-        AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
-        if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE ||
-                item.viewType == AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE) {
-            int offset = item.rowIndex > 0 ? getPaddingTop() : 0;
-            return offset + item.rowIndex * rowHeight;
-        } else {
+    protected int getTop(int rowIndex) {
+        if (getChildCount() == 0 || rowIndex <= 0) {
             return 0;
         }
+
+        // The prediction bar icons have more padding, so account for that in the row offset
+        return mPredictionIconHeight + (rowIndex - 1) * mIconHeight;
     }
 
     /**
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 4a39e6b..5097ea1 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -41,10 +41,10 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.PagedView;
-import com.android.launcher3.ShortcutInfo;
-import com.android.launcher3.util.Thunk;
-
 import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -233,6 +233,9 @@
 
         mDragObject = new DropTarget.DragObject();
 
+        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
+                registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);
+
         mDragObject.dragComplete = false;
         if (mIsAccessibleDrag) {
             // For an accessible drag, we assume the view is being dragged from the center.
@@ -242,18 +245,14 @@
         } else {
             mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
             mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
+            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
+
+            mDragDriver = DragDriver.create(this, dragInfo, dragView);
         }
 
         mDragObject.dragSource = source;
         mDragObject.dragInfo = dragInfo;
 
-        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
-                registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);
-
-        if (!accessible) {
-            mDragDriver = DragDriver.create(this, dragInfo, dragView);
-        }
-
         if (dragOffset != null) {
             dragView.setDragVisualizeOffset(new Point(dragOffset));
         }
diff --git a/src/com/android/launcher3/model/WidgetsModel.java b/src/com/android/launcher3/model/WidgetsModel.java
index eef4f91..99a53ff 100644
--- a/src/com/android/launcher3/model/WidgetsModel.java
+++ b/src/com/android/launcher3/model/WidgetsModel.java
@@ -34,19 +34,19 @@
     private static final boolean DEBUG = false;
 
     /* List of packages that is tracked by this model. */
-    private ArrayList<PackageItemInfo> mPackageItemInfos = new ArrayList<>();
+    private final ArrayList<PackageItemInfo> mPackageItemInfos;
 
     /* Map of widgets and shortcuts that are tracked per package. */
-    private HashMap<PackageItemInfo, ArrayList<Object>> mWidgetsList = new HashMap<>();
-
-    private ArrayList<Object> mRawList;
+    private final HashMap<PackageItemInfo, ArrayList<Object>> mWidgetsList;
 
     private final AppWidgetManagerCompat mAppWidgetMgr;
     private final WidgetsAndShortcutNameComparator mWidgetAndShortcutNameComparator;
     private final Comparator<ItemInfo> mAppNameComparator;
     private final IconCache mIconCache;
     private final AppFilter mAppFilter;
-    private AlphabeticIndexCompat mIndexer;
+    private final AlphabeticIndexCompat mIndexer;
+
+    private ArrayList<Object> mRawList;
 
     public WidgetsModel(Context context,  IconCache iconCache, AppFilter appFilter) {
         mAppWidgetMgr = AppWidgetManagerCompat.getInstance(context);
@@ -55,6 +55,10 @@
         mIconCache = iconCache;
         mAppFilter = appFilter;
         mIndexer = new AlphabeticIndexCompat(context);
+        mPackageItemInfos = new ArrayList<>();
+        mWidgetsList = new HashMap<>();
+
+        mRawList = new ArrayList<>();
     }
 
     @SuppressWarnings("unchecked")
@@ -62,18 +66,16 @@
         mAppWidgetMgr = model.mAppWidgetMgr;
         mPackageItemInfos = (ArrayList<PackageItemInfo>) model.mPackageItemInfos.clone();
         mWidgetsList = (HashMap<PackageItemInfo, ArrayList<Object>>) model.mWidgetsList.clone();
-        mRawList = (ArrayList<Object>) model.mRawList.clone();
         mWidgetAndShortcutNameComparator = model.mWidgetAndShortcutNameComparator;
         mAppNameComparator = model.mAppNameComparator;
         mIconCache = model.mIconCache;
         mAppFilter = model.mAppFilter;
+        mIndexer = model.mIndexer;
+        mRawList = (ArrayList<Object>) model.mRawList.clone();
     }
 
     // Access methods that may be deleted if the private fields are made package-private.
     public int getPackageSize() {
-        if (mPackageItemInfos == null) {
-            return 0;
-        }
         return mPackageItemInfos.size();
     }
 
diff --git a/src/com/android/launcher3/DummyWidget.java b/src/com/android/launcher3/testing/DummyWidget.java
similarity index 82%
rename from src/com/android/launcher3/DummyWidget.java
rename to src/com/android/launcher3/testing/DummyWidget.java
index 59cd805..df887ac 100644
--- a/src/com/android/launcher3/DummyWidget.java
+++ b/src/com/android/launcher3/testing/DummyWidget.java
@@ -1,7 +1,10 @@
-package com.android.launcher3;
+package com.android.launcher3.testing;
 
 import android.appwidget.AppWidgetProviderInfo;
 
+import com.android.launcher3.CustomAppWidget;
+import com.android.launcher3.R;
+
 public class DummyWidget implements CustomAppWidget {
     @Override
     public String getLabel() {
@@ -20,7 +23,7 @@
 
     @Override
     public int getWidgetLayout() {
-        return R.layout.dummy_widget;
+        return R.layout.zzz_dummy_widget;
     }
 
     @Override
diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java
index b4d7459..c6d5ad1 100644
--- a/src/com/android/launcher3/testing/LauncherExtension.java
+++ b/src/com/android/launcher3/testing/LauncherExtension.java
@@ -5,11 +5,13 @@
 import android.animation.ObjectAnimator;
 import android.content.ComponentName;
 import android.content.Intent;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.FrameLayout;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.InsettableFrameLayout;
@@ -210,13 +212,39 @@
         public void startVoice() {
         }
 
+        CustomContentCallbacks mCustomContentCallbacks = new CustomContentCallbacks() {
+
+            // Custom content is completely shown. {@code fromResume} indicates whether this was caused
+            // by a onResume or by scrolling otherwise.
+            public void onShow(boolean fromResume) {
+            }
+
+            // Custom content is completely hidden
+            public void onHide() {
+            }
+
+            // Custom content scroll progress changed. From 0 (not showing) to 1 (fully showing).
+            public void onScrollProgressChanged(float progress) {
+
+            }
+
+            // Indicates whether the user is allowed to scroll away from the custom content.
+            public boolean isScrollingAllowed() {
+                return true;
+            }
+
+        };
+
         @Override
         public boolean hasCustomContentToLeft() {
-            return false;
+            return true;
         }
 
         @Override
         public void populateCustomContentContainer() {
+            FrameLayout customContent = new FrameLayout(LauncherExtension.this);
+            customContent.setBackgroundColor(Color.GRAY);
+            addToCustomContentPage(customContent, mCustomContentCallbacks, "");
         }
 
         @Override
@@ -281,7 +309,7 @@
 
         @Override
         public boolean hasLauncherOverlay() {
-            return true;
+            return false;
         }
 
         @Override
diff --git a/src/com/android/launcher3/MemoryDumpActivity.java b/src/com/android/launcher3/testing/MemoryDumpActivity.java
similarity index 93%
rename from src/com/android/launcher3/MemoryDumpActivity.java
rename to src/com/android/launcher3/testing/MemoryDumpActivity.java
index d79be80..9bcf92b 100644
--- a/src/com/android/launcher3/MemoryDumpActivity.java
+++ b/src/com/android/launcher3/testing/MemoryDumpActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.testing;
 
 import android.app.Activity;
 import android.content.ComponentName;
@@ -23,10 +23,20 @@
 import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
 import android.net.Uri;
-import android.os.*;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.IBinder;
 import android.util.Log;
 
-import java.io.*;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.zip.ZipEntry;
diff --git a/src/com/android/launcher3/MemoryTracker.java b/src/com/android/launcher3/testing/MemoryTracker.java
similarity index 92%
rename from src/com/android/launcher3/MemoryTracker.java
rename to src/com/android/launcher3/testing/MemoryTracker.java
index 067a50f..ed2a312 100644
--- a/src/com/android/launcher3/MemoryTracker.java
+++ b/src/com/android/launcher3/testing/MemoryTracker.java
@@ -14,22 +14,28 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.testing;
 
 import android.app.ActivityManager;
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
-import android.os.*;
+import android.os.Binder;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.SystemClock;
 import android.util.Log;
 import android.util.LongSparseArray;
 
+import com.android.launcher3.util.TestingUtils;
+
 import java.util.ArrayList;
 import java.util.List;
 
 public class MemoryTracker extends Service {
     public static final String TAG = MemoryTracker.class.getSimpleName();
-    public static final String ACTION_START_TRACKING = "com.android.launcher3.action.START_TRACKING";
 
     private static final long UPDATE_RATE = 5000;
 
@@ -83,14 +89,6 @@
 
     ActivityManager mAm;
 
-    public static void startTrackingMe(Context context, String name) {
-        context.startService(new Intent(context, MemoryTracker.class)
-                .setAction(MemoryTracker.ACTION_START_TRACKING)
-                .putExtra("pid", android.os.Process.myPid())
-                .putExtra("name", name)
-        );
-    }
-
     public ProcessMemInfo getMemInfo(int pid) {
         return mData.get(pid);
     }
@@ -190,7 +188,7 @@
         Log.v(TAG, "Received start id " + startId + ": " + intent);
 
         if (intent != null) {
-            if (ACTION_START_TRACKING.equals(intent.getAction())) {
+            if (TestingUtils.ACTION_START_TRACKING.equals(intent.getAction())) {
                 final int pid = intent.getIntExtra("pid", -1);
                 final String name = intent.getStringExtra("name");
                 final long start = intent.getLongExtra("start", System.currentTimeMillis());
diff --git a/src/com/android/launcher3/testing/ToggleWeightWatcher.java b/src/com/android/launcher3/testing/ToggleWeightWatcher.java
new file mode 100644
index 0000000..15e55ee
--- /dev/null
+++ b/src/com/android/launcher3/testing/ToggleWeightWatcher.java
@@ -0,0 +1,32 @@
+package com.android.launcher3.testing;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.View;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.util.TestingUtils;
+
+public class ToggleWeightWatcher extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        String spKey = LauncherAppState.getSharedPreferencesKey();
+        SharedPreferences sp = getSharedPreferences(spKey, Context.MODE_PRIVATE);
+        boolean show = sp.getBoolean(TestingUtils.SHOW_WEIGHT_WATCHER, true);
+
+        show = !show;
+        sp.edit().putBoolean(TestingUtils.SHOW_WEIGHT_WATCHER, show).apply();
+
+        Launcher launcher = (Launcher) LauncherAppState.getInstance().getModel().getCallback();
+        if (launcher != null && launcher.mWeightWatcher != null) {
+            launcher.mWeightWatcher.setVisibility(show ? View.VISIBLE : View.GONE);
+        }
+        finish();
+    }
+}
diff --git a/src/com/android/launcher3/WeightWatcher.java b/src/com/android/launcher3/testing/WeightWatcher.java
similarity index 98%
rename from src/com/android/launcher3/WeightWatcher.java
rename to src/com/android/launcher3/testing/WeightWatcher.java
index 7568479..a26a2b6 100644
--- a/src/com/android/launcher3/WeightWatcher.java
+++ b/src/com/android/launcher3/testing/WeightWatcher.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3;
+package com.android.launcher3.testing;
 
 import android.content.ComponentName;
 import android.content.Context;
@@ -116,10 +116,6 @@
         }
     }
 
-    public WeightWatcher(Context context) {
-        this(context, null);
-    }
-
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
diff --git a/src/com/android/launcher3/util/TestingUtils.java b/src/com/android/launcher3/util/TestingUtils.java
new file mode 100644
index 0000000..39b8046
--- /dev/null
+++ b/src/com/android/launcher3/util/TestingUtils.java
@@ -0,0 +1,73 @@
+package com.android.launcher3.util;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.CustomAppWidget;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
+
+import java.util.HashMap;
+
+public class TestingUtils {
+
+    public static final String MEMORY_TRACKER = "com.android.launcher3.testing.MemoryTracker";
+    public static final String ACTION_START_TRACKING = "com.android.launcher3.action.START_TRACKING";
+
+    public static final boolean MEMORY_DUMP_ENABLED = false;
+    public static final String SHOW_WEIGHT_WATCHER = "debug.show_mem";
+
+    public static final boolean ENABLE_CUSTOM_WIDGET_TEST = false;
+    public static final String DUMMY_WIDGET = "com.android.launcher3.testing.DummyWidget";
+
+    public static void startTrackingMemory(Context context) {
+        if (MEMORY_DUMP_ENABLED) {
+            context.startService(new Intent()
+                .setComponent(new ComponentName(context.getPackageName(), MEMORY_TRACKER))
+                .setAction(ACTION_START_TRACKING)
+                .putExtra("pid", android.os.Process.myPid())
+                .putExtra("name", "L"));
+        }
+    }
+
+    public static void addWeightWatcher(Launcher launcher) {
+        if (MEMORY_DUMP_ENABLED) {
+            String spKey = LauncherAppState.getSharedPreferencesKey();
+            SharedPreferences sp = launcher.getSharedPreferences(spKey, Context.MODE_PRIVATE);
+            boolean show = sp.getBoolean(SHOW_WEIGHT_WATCHER, true);
+
+            int id = launcher.getResources().getIdentifier("zzz_weight_watcher", "layout",
+                    launcher.getPackageName());
+            View watcher = launcher.getLayoutInflater().inflate(id, null);
+            watcher.setAlpha(0.5f);
+            ((FrameLayout) launcher.findViewById(R.id.launcher)).addView(watcher,
+                    new FrameLayout.LayoutParams(
+                            FrameLayout.LayoutParams.MATCH_PARENT,
+                            FrameLayout.LayoutParams.WRAP_CONTENT,
+                            Gravity.BOTTOM)
+            );
+
+            watcher.setVisibility(show ? View.VISIBLE : View.GONE);
+            launcher.mWeightWatcher = watcher;
+        }
+    }
+
+    public static void addDummyWidget(HashMap<String, CustomAppWidget> set) {
+        if (ENABLE_CUSTOM_WIDGET_TEST) {
+            try {
+                Class<?> clazz = Class.forName(DUMMY_WIDGET);
+                CustomAppWidget widget = (CustomAppWidget) clazz.newInstance();
+                set.put(widget.getClass().getName(), widget);
+            } catch (Exception e) {
+                Log.e("TestingUtils", "Error adding dummy widget", e);
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
new file mode 100644
index 0000000..01808ba
--- /dev/null
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -0,0 +1,96 @@
+/**
+ * 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.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import com.android.launcher3.DeferredHandler;
+import com.android.launcher3.Launcher;
+
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * An executor which runs all the tasks after the first onDraw is called on the target view.
+ */
+public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable,
+        OnAttachStateChangeListener {
+
+    private final ArrayList<Runnable> mTasks = new ArrayList<>();
+    private final DeferredHandler mHandler;
+
+    private Launcher mLauncher;
+    private View mAttachedView;
+    private boolean mCompleted;
+
+    public ViewOnDrawExecutor(DeferredHandler handler) {
+        mHandler = handler;
+    }
+
+    public void attachTo(Launcher launcher) {
+        mLauncher = launcher;
+        mAttachedView = launcher.getWorkspace();
+        mAttachedView.addOnAttachStateChangeListener(this);
+
+        attachObserver();
+    }
+
+    private void attachObserver() {
+        if (!mCompleted) {
+            mAttachedView.getViewTreeObserver().addOnDrawListener(this);
+        }
+    }
+
+    @Override
+    public void execute(Runnable command) {
+        mTasks.add(command);
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View v) {
+        attachObserver();
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View v) { }
+
+    @Override
+    public void onDraw() {
+        mAttachedView.post(this);
+    }
+
+    @Override
+    public void run() {
+        for (final Runnable r : mTasks) {
+            mHandler.post(r);
+        }
+        markCompleted();
+    }
+
+    public void markCompleted() {
+        mTasks.clear();
+        if (mAttachedView != null) {
+            mAttachedView.getViewTreeObserver().removeOnDrawListener(this);
+            mAttachedView.removeOnAttachStateChangeListener(this);
+        }
+        if (mLauncher != null) {
+            mLauncher.clearPendingExecutor(this);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index 81a8465..0f5ca15 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -148,8 +148,11 @@
         if (mWidgetInstructionToast != null) {
             mWidgetInstructionToast.cancel();
         }
-        mWidgetInstructionToast = Toast.makeText(getContext(),R.string.long_press_widget_to_add,
-                Toast.LENGTH_SHORT);
+
+        CharSequence msg = Utilities.wrapForTts(
+                getContext().getText(R.string.long_press_widget_to_add),
+                getContext().getString(R.string.long_accessible_way_to_add));
+        mWidgetInstructionToast = Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT);
         mWidgetInstructionToast.show();
     }
 
@@ -164,7 +167,6 @@
         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;
 
         boolean status = beginDragging(v);
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 884bdc4..fe9c51c 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -102,9 +102,9 @@
         // Stop the scroller if it is scrolling
         stopScroll();
 
-        getCurScrollState(mScrollPosState);
+        getCurScrollState(mScrollPosState, -1);
         float pos = rowCount * touchFraction;
-        int availableScrollHeight = getAvailableScrollHeight(rowCount, mScrollPosState.rowHeight);
+        int availableScrollHeight = getAvailableScrollHeight(rowCount);
         LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager());
         layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction));
 
@@ -131,7 +131,7 @@
         }
 
         // Skip early if, there no child laid out in the container.
-        getCurScrollState(mScrollPosState);
+        getCurScrollState(mScrollPosState, -1);
         if (mScrollPosState.rowIndex < 0) {
             mScrollbar.setThumbOffset(-1, -1);
             return;
@@ -143,10 +143,10 @@
     /**
      * Returns the current scroll state.
      */
-    protected void getCurScrollState(ScrollPositionState stateOut) {
+    protected void getCurScrollState(ScrollPositionState stateOut, int viewTypeMask) {
         stateOut.rowIndex = -1;
         stateOut.rowTopOffset = -1;
-        stateOut.rowHeight = -1;
+        stateOut.itemPos = -1;
 
         // Skip early if widgets are not bound.
         if (mWidgets == null) {
@@ -163,6 +163,17 @@
 
         stateOut.rowIndex = position;
         stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
-        stateOut.rowHeight = child.getHeight();
+        stateOut.itemPos = position;
+    }
+
+    @Override
+    protected int getTop(int rowIndex) {
+        if (getChildCount() == 0) {
+            return 0;
+        }
+
+        // All the rows are the same height, return any child height
+        View child = getChildAt(0);
+        return child.getMeasuredHeight() * rowIndex;
     }
 }
\ No newline at end of file