Merge "Stylus support: handling drag n drop for widgets" into ub-launcher3-burnaby
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b61b90c..fce4691 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -83,6 +83,7 @@
             android:theme="@style/Theme"
             android:windowSoftInputMode="adjustPan"
             android:screenOrientation="nosensor"
+            android:resumeWhilePausing="true"
             android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -148,6 +149,12 @@
             </intent-filter>
         </activity>
 
+        <activity
+            android:name="com.android.launcher3.SettingsActivity"
+            android:label="@string/settings_button_text"
+            android:autoRemoveFromRecents="true">
+        </activity>
+
         <!-- Debugging tools -->
         <activity
             android:name="com.android.launcher3.MemoryDumpActivity"
diff --git a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
index 3f90203..9415941 100644
--- a/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
+++ b/WallpaperPicker/src/com/android/launcher3/WallpaperPickerActivity.java
@@ -49,6 +49,7 @@
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLayoutChangeListener;
@@ -891,6 +892,15 @@
 
     private void addLongPressHandler(View v) {
         v.setOnLongClickListener(mLongClickListener);
+
+        // Enable stylus button to also trigger long click.
+        final StylusEventHelper stylusEventHelper = new StylusEventHelper(v);
+        v.setOnTouchListener(new View.OnTouchListener() {
+            @Override
+            public boolean onTouch(View view, MotionEvent event) {
+                return stylusEventHelper.checkAndPerformStylusEvent(event);
+            }
+        });
     }
 
     private ArrayList<WallpaperTileInfo> findBundledWallpapers() {
@@ -1132,6 +1142,11 @@
 
     @Override
     public boolean enableRotation() {
-        return Utilities.isRotationEnabled(getContext());
+        // Check if rotation is enabled for this device.
+        if (Utilities.isRotationAllowedForDevice(getContext()))
+            return true;
+
+        // Check if the user has specifically enabled rotation via preferences.
+        return Utilities.isAllowRotationPrefEnabled(getApplicationContext());
     }
 }
diff --git a/res/layout/user_folder.xml b/res/layout/user_folder.xml
index 67b69ca..ecf7def 100644
--- a/res/layout/user_folder.xml
+++ b/res/layout/user_folder.xml
@@ -40,7 +40,7 @@
             android:layout_height="match_parent"
             android:paddingLeft="4dp"
             android:paddingRight="4dp"
-            android:paddingTop="4dp"
+            android:paddingTop="8dp"
             launcher:pageIndicator="@+id/folder_page_indicator" />
     </FrameLayout>
 
@@ -48,6 +48,7 @@
         android:id="@+id/folder_footer"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:clipChildren="false"
         android:orientation="horizontal"
         android:paddingLeft="8dp"
         android:paddingRight="8dp" >
@@ -63,8 +64,8 @@
             android:gravity="center_horizontal"
             android:hint="@string/folder_hint_text"
             android:imeOptions="flagNoExtractUi"
-            android:paddingBottom="@dimen/folder_name_padding"
-            android:paddingTop="@dimen/folder_name_padding"
+            android:paddingBottom="8dp"
+            android:paddingTop="4dp"
             android:singleLine="true"
             android:textColor="#ff777777"
             android:textColorHighlight="#ffCCCCCC"
@@ -78,6 +79,7 @@
             android:layout_height="12dp"
             android:layout_gravity="center_vertical"
             layout="@layout/page_indicator" />
+
     </LinearLayout>
 
 </com.android.launcher3.Folder>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 246adcd..7950862 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -114,7 +114,6 @@
 <!-- Folders -->
     <!-- The amount that the preview contents are inset from the preview background -->
     <dimen name="folder_preview_padding">4dp</dimen>
-    <dimen name="folder_name_padding">10dp</dimen>
 
 <!-- Sizes for managed profile badges -->
     <dimen name="profile_badge_size">24dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a8c668d..440a537 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -166,6 +166,12 @@
     <!-- Text for settings button -->
     <string name="settings_button_text">Settings</string>
 
+    <!-- Strings for settings -->
+    <!-- Title for Allow Rotation setting. [CHAR LIMIT=50] [DO NOT TRANSLATE] -->
+    <string name="allow_rotation_title">Allow rotation</string>
+    <!-- Summary for Allow Rotation setting. [CHAR LIMIT=150] [DO NOT TRANSLATE] -->
+    <string name="allow_rotation_summary">Allow rotation of the home screen</string>
+
     <!-- Label on an icon that references an uninstalled package, for which we have no information about when it might be installed. [CHAR_LIMIT=15] -->
     <string name="package_state_unknown">Unknown</string>
 
diff --git a/res/xml/launcher_preferences.xml b/res/xml/launcher_preferences.xml
new file mode 100644
index 0000000..f283575
--- /dev/null
+++ b/res/xml/launcher_preferences.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 Google Inc.
+
+     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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <SwitchPreference
+            android:key="pref_allowRotation"
+            android:title="@string/allow_rotation_title"
+            android:summary="@string/allow_rotation_summary"
+            android:defaultValue="@bool/allow_rotation"
+            android:persistent="true"
+    />
+
+</PreferenceScreen>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index edf5021..314c21f 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -28,7 +28,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.KeyEvent;
@@ -36,7 +35,6 @@
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
 import android.widget.TextView;
-
 import com.android.launcher3.IconCache.IconLoadRequest;
 import com.android.launcher3.model.PackageItemInfo;
 
@@ -63,6 +61,7 @@
     private final Drawable mBackground;
     private final CheckLongPressHelper mLongPressHelper;
     private final HolographicOutlineHelper mOutlineHelper;
+    private final StylusEventHelper mStylusEventHelper;
 
     private boolean mBackgroundSizeChanged;
 
@@ -125,6 +124,7 @@
         }
 
         mLongPressHelper = new CheckLongPressHelper(this);
+        mStylusEventHelper = new StylusEventHelper(this);
 
         mOutlineHelper = HolographicOutlineHelper.obtain(getContext());
         if (mCustomShadowsEnabled) {
@@ -236,6 +236,12 @@
         // isPressed() on an ACTION_UP
         boolean result = super.onTouchEvent(event);
 
+        // Check for a stylus button press, if it occurs cancel any long press checks.
+        if (mStylusEventHelper.checkAndPerformStylusEvent(event)) {
+            mLongPressHelper.cancelLongPress();
+            result = true;
+        }
+
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 // So that the pressed outline is visible immediately on setStayPressed(),
@@ -245,7 +251,10 @@
                     mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
                 }
 
-                mLongPressHelper.postCheckForLongPress();
+                // If we're in a stylus button press, don't check for long press.
+                if (!mStylusEventHelper.inStylusButtonPressed()) {
+                    mLongPressHelper.postCheckForLongPress();
+                }
                 break;
             case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 4cd28c0..09a71b0 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -64,7 +64,7 @@
     protected Drawable mDrawable;
 
     private AnimatorSet mCurrentColorAnim;
-    private ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter;
+    @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter;
 
 
     public ButtonDropTarget(Context context, AttributeSet attrs) {
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 61567ac..b5d0dca 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -95,6 +95,7 @@
     boolean[][] mTmpOccupied;
 
     private OnTouchListener mInterceptTouchListener;
+    private StylusEventHelper mStylusEventHelper;
 
     private ArrayList<FolderRingAnimator> mFolderOuterRings = new ArrayList<FolderRingAnimator>();
     private int[] mFolderLeaveBehindCell = {-1, -1};
@@ -130,10 +131,8 @@
 
     private final ClickShadowView mTouchFeedbackView;
 
-    @Thunk HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new
-            HashMap<CellLayout.LayoutParams, Animator>();
-    private HashMap<View, ReorderPreviewAnimation>
-            mShakeAnimators = new HashMap<View, ReorderPreviewAnimation>();
+    @Thunk HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new HashMap<>();
+    @Thunk HashMap<View, ReorderPreviewAnimation> mShakeAnimators = new HashMap<>();
 
     private boolean mItemPlacementDirty = false;
 
@@ -286,6 +285,8 @@
         mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap,
                 mCountX, mCountY);
 
+        mStylusEventHelper = new StylusEventHelper(this);
+
         mTouchFeedbackView = new ClickShadowView(context);
         addView(mTouchFeedbackView);
         addView(mShortcutsAndWidgets);
@@ -338,6 +339,20 @@
         return false;
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean handled = super.onTouchEvent(ev);
+        // Stylus button press on a home screen should not switch between overview mode and
+        // the home screen mode, however, once in overview mode stylus button press should be
+        // enabled to allow rearranging the different home screens. So check what mode
+        // the workspace is in, and only perform stylus button presses while in overview mode.
+        if (mLauncher.mWorkspace.isInOverviewMode()
+                && mStylusEventHelper.checkAndPerformStylusEvent(ev)) {
+            return true;
+        }
+        return handled;
+    }
+
     public void enableHardwareLayer(boolean hasLayer) {
         mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint);
     }
diff --git a/src/com/android/launcher3/CommonAppTypeParser.java b/src/com/android/launcher3/CommonAppTypeParser.java
index 3164179..5314ecf 100644
--- a/src/com/android/launcher3/CommonAppTypeParser.java
+++ b/src/com/android/launcher3/CommonAppTypeParser.java
@@ -26,6 +26,7 @@
 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.backup.BackupProtos.Favorite;
+import com.android.launcher3.util.Thunk;
 
 import org.xmlpull.v1.XmlPullParserException;
 
@@ -44,8 +45,8 @@
 
 
     private final long mItemId;
-    private final int mResId;
-    private final Context mContext;
+    @Thunk final int mResId;
+    @Thunk final Context mContext;
 
     ContentValues parsedValues;
     Intent parsedIntent;
diff --git a/src/com/android/launcher3/DragView.java b/src/com/android/launcher3/DragView.java
index b332338..dfa8202 100644
--- a/src/com/android/launcher3/DragView.java
+++ b/src/com/android/launcher3/DragView.java
@@ -44,7 +44,7 @@
 
     private Bitmap mBitmap;
     private Bitmap mCrossFadeBitmap;
-    private Paint mPaint;
+    @Thunk Paint mPaint;
     private int mRegistrationX;
     private int mRegistrationY;
 
@@ -62,7 +62,7 @@
     // size.  This is ignored for non-icons.
     private float mIntrinsicIconScale = 1f;
 
-    private float[] mCurrentFilter;
+    @Thunk float[] mCurrentFilter;
     private ValueAnimator mFilterAnimator;
 
     /**
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index 46e4902..70bb01a 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -426,7 +426,7 @@
     /**
      * Private helper method to get the CellLayoutChildren given a CellLayout index.
      */
-    private static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
+    @Thunk static ShortcutAndWidgetContainer getCellLayoutChildrenForIndex(
             ViewGroup container, int i) {
         CellLayout parent = (CellLayout) container.getChildAt(i);
         return parent.getShortcutsAndWidgets();
diff --git a/src/com/android/launcher3/Folder.java b/src/com/android/launcher3/Folder.java
index ec4ea04..94f8fc8 100644
--- a/src/com/android/launcher3/Folder.java
+++ b/src/com/android/launcher3/Folder.java
@@ -29,6 +29,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Build;
+import android.os.Bundle;
 import android.text.InputType;
 import android.text.Selection;
 import android.text.Spannable;
@@ -44,6 +45,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AnimationUtils;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.LinearLayout;
@@ -65,7 +67,8 @@
  */
 public class Folder extends LinearLayout implements DragSource, View.OnClickListener,
         View.OnLongClickListener, DropTarget, FolderListener, TextView.OnEditorActionListener,
-        View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource {
+        View.OnFocusChangeListener, DragListener, UninstallSource, AccessibilityDragSource,
+        Stats.LaunchSourceProvider {
     private static final String TAG = "Launcher.Folder";
 
     /**
@@ -89,7 +92,7 @@
      */
     private static final float ICON_OVERSCROLL_WIDTH_FACTOR = 0.45f;
 
-    public static final int FOOTER_ANIMATION_DURATION = 200;
+    private static final int FOLDER_NAME_ANIMATION_DURATION = 633;
 
     private static final int REORDER_DELAY = 250;
     private static final int ON_EXIT_CLOSE_DELAY = 400;
@@ -502,7 +505,6 @@
             textAlpha.setStartDelay(mMaterialExpandStagger);
             textAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
 
-
             anim.play(drift);
             anim.play(iconsAlpha);
             anim.play(textAlpha);
@@ -545,7 +547,8 @@
                     - mFooter.getPaddingLeft() - mFooter.getPaddingRight();
 
             float textWidth =  mFolderName.getPaint().measureText(mFolderName.getText().toString());
-            mFolderName.setTranslationX((footerWidth - textWidth) / 2);
+            float translation = (footerWidth - textWidth) / 2;
+            mFolderName.setTranslationX(mContent.mIsRtl ? -translation : translation);
             mContent.setMarkerScale(0);
 
             // Do not update the flag if we are in drag mode. The flag will be updated, when we
@@ -555,7 +558,12 @@
 
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    mFolderName.animate().setDuration(FOOTER_ANIMATION_DURATION).translationX(0);
+                    mFolderName.animate().setDuration(FOLDER_NAME_ANIMATION_DURATION)
+                        .translationX(0)
+                        .setInterpolator(Utilities.isLmpOrAbove() ?
+                                AnimationUtils.loadInterpolator(mLauncher,
+                                        android.R.interpolator.fast_out_slow_in)
+                                : new LogDecelerateInterpolator(100, 0));
                     mContent.animateMarkers();
 
                     if (updateAnimationFlag) {
@@ -917,7 +925,7 @@
             View v = list.get(i);
             ItemInfo info = (ItemInfo) v.getTag();
             LauncherModel.addItemToDatabase(mLauncher, info, mInfo.id, 0,
-                        info.cellX, info.cellY);
+                    info.cellX, info.cellY);
         }
     }
 
@@ -1032,6 +1040,15 @@
 
         mContent.setFixedSize(contentWidth, contentHeight);
         mContentWrapper.measure(contentAreaWidthSpec, contentAreaHeightSpec);
+
+        if (mContent.getChildCount() > 0) {
+            int cellIconGap = (mContent.getPageAt(0).getCellWidth()
+                    - mLauncher.getDeviceProfile().iconSizePx) / 2;
+            mFooter.setPadding(mContent.getPaddingLeft() + cellIconGap,
+                    mFooter.getPaddingTop(),
+                    mContent.getPaddingRight() + cellIconGap,
+                    mFooter.getPaddingBottom());
+        }
         mFooter.measure(contentAreaWidthSpec,
                 MeasureSpec.makeMeasureSpec(mFooterHeight, MeasureSpec.EXACTLY));
 
@@ -1323,6 +1340,14 @@
         outRect.right += mScrollAreaOffset;
     }
 
+    @Override
+    public void fillInLaunchSourceData(Bundle sourceData) {
+        // Fill in from the folder icon's launch source provider first
+        Stats.LaunchSourceUtils.populateSourceDataFromAncestorProvider(mFolderIcon, sourceData);
+        sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER, Stats.SUB_CONTAINER_FOLDER);
+        sourceData.putInt(Stats.SOURCE_EXTRA_SUB_CONTAINER_PAGE, mContent.getCurrentPage());
+    }
+
     private class OnScrollHintListener implements OnAlarmListener {
 
         private final DragObject mDragObject;
diff --git a/src/com/android/launcher3/FolderIcon.java b/src/com/android/launcher3/FolderIcon.java
index 8652eef..a81e651 100644
--- a/src/com/android/launcher3/FolderIcon.java
+++ b/src/com/android/launcher3/FolderIcon.java
@@ -57,6 +57,7 @@
     @Thunk static boolean sStaticValuesDirty = true;
 
     private CheckLongPressHelper mLongPressHelper;
+    private StylusEventHelper mStylusEventHelper;
 
     // The number of icons to display in the
     public static final int NUM_ITEMS_IN_PREVIEW = 3;
@@ -128,6 +129,7 @@
 
     private void init() {
         mLongPressHelper = new CheckLongPressHelper(this);
+        mStylusEventHelper = new StylusEventHelper(this);
         setAccessibilityDelegate(LauncherAppState.getInstance().getAccessibilityDelegate());
     }
 
@@ -719,6 +721,12 @@
         // isPressed() on an ACTION_UP
         boolean result = super.onTouchEvent(event);
 
+        // Check for a stylus button press, if it occurs cancel any long press checks.
+        if (mStylusEventHelper.checkAndPerformStylusEvent(event)) {
+            mLongPressHelper.cancelLongPress();
+            return true;
+        }
+
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
                 mLongPressHelper.postCheckForLongPress();
diff --git a/src/com/android/launcher3/FolderPagedView.java b/src/com/android/launcher3/FolderPagedView.java
index 0bd6501..7d90ba2 100644
--- a/src/com/android/launcher3/FolderPagedView.java
+++ b/src/com/android/launcher3/FolderPagedView.java
@@ -24,6 +24,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
 import android.view.animation.OvershootInterpolator;
 
 import com.android.launcher3.FocusHelper.PagedFolderKeyEventListener;
@@ -46,7 +47,12 @@
     private static final int START_VIEW_REORDER_DELAY = 30;
     private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
 
-    private static final int PAGE_INDICATOR_ANIMATION_DELAY = 150;
+    private static final int PAGE_INDICATOR_ANIMATION_START_DELAY = 300;
+    private static final int PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY = 150;
+    private static final int PAGE_INDICATOR_ANIMATION_DURATION = 400;
+
+    // This value approximately overshoots to 1.5 times the original size.
+    private static final float PAGE_INDICATOR_OVERSHOOT_TENSION = 4.9f;
 
     /**
      * Fraction of the width to scroll when showing the next page hint.
@@ -274,6 +280,7 @@
         arrangeChildren(list, itemCount, true);
     }
 
+    @SuppressLint("RtlHardcoded")
     private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
         ArrayList<CellLayout> pages = new ArrayList<CellLayout>();
         for (int i = 0; i < getChildCount(); i++) {
@@ -340,7 +347,9 @@
 
         // Update footer
         mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
-        mFolder.mFolderName.setGravity(getPageCount() > 1 ? Gravity.START : Gravity.CENTER_HORIZONTAL);
+        // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
+        mFolder.mFolderName.setGravity(getPageCount() > 1 ?
+                (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
     }
 
     public int getDesiredWidth() {
@@ -645,12 +654,13 @@
 
     public void animateMarkers() {
         int count  = mPageIndicator.getChildCount();
-        OvershootInterpolator interpolator = new OvershootInterpolator(4);
+        Interpolator interpolator = new OvershootInterpolator(PAGE_INDICATOR_OVERSHOOT_TENSION);
         for (int i = 0; i < count; i++) {
             mPageIndicator.getChildAt(i).animate().scaleX(1).scaleY(1)
                 .setInterpolator(interpolator)
-                .setDuration(Folder.FOOTER_ANIMATION_DURATION)
-                .setStartDelay(PAGE_INDICATOR_ANIMATION_DELAY * i);
+                .setDuration(PAGE_INDICATOR_ANIMATION_DURATION)
+                .setStartDelay(PAGE_INDICATOR_ANIMATION_STAGGERED_DELAY * i
+                        + PAGE_INDICATOR_ANIMATION_START_DELAY);
         }
     }
 
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index ce33164..6f09744 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -19,15 +19,16 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
-public class Hotseat extends FrameLayout {
+public class Hotseat extends FrameLayout
+        implements Stats.LaunchSourceProvider{
 
     private CellLayout mContent;
 
@@ -160,4 +161,9 @@
         }
         return false;
     }
+
+    @Override
+    public void fillInLaunchSourceData(Bundle sourceData) {
+        sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOTSEAT);
+    }
 }
diff --git a/src/com/android/launcher3/IconCache.java b/src/com/android/launcher3/IconCache.java
index 6dfca9e..a16067d 100644
--- a/src/com/android/launcher3/IconCache.java
+++ b/src/com/android/launcher3/IconCache.java
@@ -69,7 +69,7 @@
 
     private static final int LOW_RES_SCALE_FACTOR = 8;
 
-    private static final Object ICON_UPDATE_TOKEN = new Object();
+    @Thunk static final Object ICON_UPDATE_TOKEN = new Object();
 
     @Thunk static class CacheEntry {
         public Bitmap icon;
@@ -79,18 +79,18 @@
     }
 
     private final HashMap<UserHandleCompat, Bitmap> mDefaultIcons = new HashMap<>();
-    private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
+    @Thunk final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
 
     private final Context mContext;
     private final PackageManager mPackageManager;
-    private final UserManagerCompat mUserManager;
+    @Thunk final UserManagerCompat mUserManager;
     private final LauncherAppsCompat mLauncherApps;
     private final HashMap<ComponentKey, CacheEntry> mCache =
             new HashMap<ComponentKey, CacheEntry>(INITIAL_ICON_CACHE_CAPACITY);
     private final int mIconDpi;
-    private final IconDB mIconDb;
+    @Thunk final IconDB mIconDb;
 
-    private final Handler mWorkerHandler;
+    @Thunk final Handler mWorkerHandler;
 
     public IconCache(Context context, InvariantDeviceProfile inv) {
         ActivityManager activityManager =
@@ -320,7 +320,7 @@
         }
     }
 
-    private void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info,
+    @Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info,
             long userSerial) {
         // Reuse the existing entry if it already exists in the DB. This ensures that we do not
         // create bitmap if it was already created during loader.
@@ -342,7 +342,7 @@
                 SQLiteDatabase.CONFLICT_REPLACE);
     }
 
-    private ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app,
+    @Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app,
             boolean replaceExisting) {
         final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser());
         CacheEntry entry = null;
@@ -688,14 +688,14 @@
      * LauncherActivityInfoCompat list. Items are updated/added one at a time, so that the
      * worker thread doesn't get blocked.
      */
-    private class SerializedIconUpdateTask implements Runnable {
+    @Thunk class SerializedIconUpdateTask implements Runnable {
         private final long mUserSerial;
         private final HashMap<String, PackageInfo> mPkgInfoMap;
         private final Stack<LauncherActivityInfoCompat> mAppsToAdd;
         private final Stack<LauncherActivityInfoCompat> mAppsToUpdate;
         private final HashSet<String> mUpdatedPackages = new HashSet<String>();
 
-        private SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap,
+        @Thunk SerializedIconUpdateTask(long userSerial, HashMap<String, PackageInfo> pkgInfoMap,
                 Stack<LauncherActivityInfoCompat> appsToAdd,
                 Stack<LauncherActivityInfoCompat> appsToUpdate) {
             mUserSerial = userSerial;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5dac3b3..867a6e7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -100,6 +100,7 @@
 import com.android.launcher3.PagedView.PageSwitchListener;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.allapps.AppSearchManager;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherActivityInfoCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
@@ -218,7 +219,8 @@
     public static final String USER_HAS_MIGRATED = "launcher.user_migrated_from_old_data";
 
     /** The different states that Launcher can be in. */
-    enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED };
+    enum State { NONE, WORKSPACE, APPS, APPS_SPRING_LOADED, WIDGETS, WIDGETS_SPRING_LOADED }
+
     @Thunk State mState = State.WORKSPACE;
     @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation;
 
@@ -263,7 +265,7 @@
 
     private int[] mTmpAddItemCellCoordinates = new int[2];
 
-    private Hotseat mHotseat;
+    @Thunk Hotseat mHotseat;
     private ViewGroup mOverviewPanel;
 
     private View mAllAppsButton;
@@ -400,6 +402,24 @@
 
     FocusIndicatorView mFocusHandler;
 
+    @Thunk boolean mRotationEnabled = false;
+    private boolean mPreferenceObserverRegistered = false;
+
+    final private SharedPreferences.OnSharedPreferenceChangeListener mSettingsObserver =
+            new SharedPreferences.OnSharedPreferenceChangeListener() {
+        @Override
+        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+            if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(key)) {
+                if (mRotationEnabled = sharedPreferences.getBoolean(
+                        Utilities.ALLOW_ROTATION_PREFERENCE_KEY, false)) {
+                    unlockScreenOrientation(true);
+                } else {
+                    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
+                }
+            }
+        }
+    };
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         if (DEBUG_STRICT_MODE) {
@@ -499,7 +519,19 @@
         IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
         registerReceiver(mCloseSystemDialogsReceiver, filter);
 
-        // On large interfaces, we want the screen to auto-rotate based on the current orientation
+        mRotationEnabled = Utilities.isRotationAllowedForDevice(getApplicationContext());
+        // In case we are on a device with locked rotation, we should look at preferences to check
+        // if the user has specifically allowed rotation.
+        if (!mRotationEnabled) {
+            getSharedPreferences(LauncherFiles.ROTATION_PREF_FILE,
+                    Context.MODE_MULTI_PROCESS).registerOnSharedPreferenceChangeListener(
+                    mSettingsObserver);
+            mPreferenceObserverRegistered = true;
+            mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext());
+        }
+
+        // On large interfaces, or on devices that a user has specifically enabled screen rotation,
+        // we want the screen to auto-rotate based on the current orientation
         unlockScreenOrientation(true);
 
         if (mLauncherCallbacks != null) {
@@ -552,6 +584,11 @@
                     }
                 }
             }
+
+            @Override
+            public void setSearchManager(AppSearchManager manager) {
+                mAppsView.setSearchManager(manager);
+            }
         });
         mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() {
             private boolean mImportanceStored = false;
@@ -1127,6 +1164,11 @@
          * Called to dismiss all apps if it is showing.
          */
         public void dismissAllApps();
+
+        /**
+         * Sets the search manager to be used for app search.
+         */
+        public void setSearchManager(AppSearchManager manager);
     }
 
     public interface LauncherSearchCallbacks {
@@ -1192,8 +1234,11 @@
     protected boolean hasSettings() {
         if (mLauncherCallbacks != null) {
             return mLauncherCallbacks.hasSettings();
+        } else {
+            // On devices with a locked orientation, we will at least have the allow rotation
+            // setting.
+            return !Utilities.isRotationAllowedForDevice(this);
         }
-        return false;
     }
 
     public void addToCustomContentPage(View customContent,
@@ -1486,7 +1531,6 @@
      * Add a shortcut to the workspace.
      *
      * @param data The intent describing the shortcut.
-     * @param cellInfo The position on screen where to create the shortcut.
      */
     private void completeAddShortcut(Intent data, long container, long screenId, int cellX,
             int cellY) {
@@ -1975,6 +2019,13 @@
     public void onDestroy() {
         super.onDestroy();
 
+        if (mPreferenceObserverRegistered) {
+            getSharedPreferences(LauncherFiles.ROTATION_PREF_FILE,
+                    Context.MODE_MULTI_PROCESS).unregisterOnSharedPreferenceChangeListener(
+                    mSettingsObserver);
+            mPreferenceObserverRegistered = false;
+        }
+
         // Remove all pending runnables
         mHandler.removeMessages(ADVANCE_MSG);
         mHandler.removeMessages(0);
@@ -2497,13 +2548,6 @@
         }
     }
 
-    public void onClickPagedViewIcon(View v) {
-        startAppShortcutOrInfoActivity(v);
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onClickPagedViewIcon(v);
-        }
-    }
-
     @SuppressLint("ClickableViewAccessibility")
     public boolean onTouch(View v, MotionEvent event) {
         return false;
@@ -2662,7 +2706,7 @@
         }
 
         boolean success = startActivitySafely(v, intent, tag);
-        mStats.recordLaunch(intent, shortcut);
+        mStats.recordLaunch(v, intent, shortcut);
 
         if (success && v instanceof BubbleTextView) {
             mWaitingForResume = (BubbleTextView) v;
@@ -2757,6 +2801,8 @@
         if (LOGD) Log.d(TAG, "onClickSettingsButton");
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onClickSettingsButton(v);
+        } else {
+            showSettingsActivity();
         }
     }
 
@@ -2882,7 +2928,7 @@
         }
     }
 
-    boolean startActivity(View v, Intent intent, Object tag) {
+    private boolean startActivity(View v, Intent intent, Object tag) {
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         try {
             // Only launch using the new animation if the shortcut has not opted out (this is a
@@ -2953,7 +2999,7 @@
         return false;
     }
 
-    boolean startActivitySafely(View v, Intent intent, Object tag) {
+    private boolean startActivitySafely(View v, Intent intent, Object tag) {
         boolean success = false;
         if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) {
             Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
@@ -4364,7 +4410,7 @@
     }
 
     public void lockScreenOrientation() {
-        if (Utilities.isRotationEnabled(this)) {
+        if (mRotationEnabled) {
             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
                 setRequestedOrientation(mapConfigurationOriActivityInfoOri(getResources()
                         .getConfiguration().orientation));
@@ -4373,8 +4419,9 @@
             }
         }
     }
+
     public void unlockScreenOrientation(boolean immediate) {
-        if (Utilities.isRotationEnabled(this)) {
+        if (mRotationEnabled) {
             if (immediate) {
                 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
             } else {
@@ -4479,6 +4526,10 @@
         editor.apply();
     }
 
+    private void showSettingsActivity() {
+        startActivity(new Intent(this, SettingsActivity.class));
+    }
+
     /**
      * To be overridden by subclasses to indicate that there is an in-activity full-screen intro
      * screen that must be displayed and dismissed.
diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java
index af41012..b40ace3 100644
--- a/src/com/android/launcher3/LauncherBackupHelper.java
+++ b/src/com/android/launcher3/LauncherBackupHelper.java
@@ -51,13 +51,13 @@
 import com.android.launcher3.backup.BackupProtos.Widget;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.Thunk;
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 import com.google.protobuf.nano.MessageNano;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.io.ByteArrayOutputStream;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -135,7 +135,7 @@
 
     private static final int SCREEN_RANK_INDEX = 2;
 
-    private final Context mContext;
+    @Thunk final Context mContext;
     private final HashSet<String> mExistingKeys;
     private final ArrayList<Key> mKeys;
     private final ItemTypeMatcher[] mItemTypeMatchers;
@@ -1157,15 +1157,15 @@
                 .getSerialNumberForUser(UserHandleCompat.myUserHandle());
     }
 
-    private class InvalidBackupException extends IOException {
+    @Thunk class InvalidBackupException extends IOException {
 
         private static final long serialVersionUID = 8931456637211665082L;
 
-        private InvalidBackupException(Throwable cause) {
+        @Thunk InvalidBackupException(Throwable cause) {
             super(cause);
         }
 
-        public InvalidBackupException(String reason) {
+        @Thunk InvalidBackupException(String reason) {
             super(reason);
         }
     }
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index a5f36ba..70e400b 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -56,6 +56,8 @@
     public void bindAllApplications(ArrayList<AppInfo> apps);
     public void onClickFolderIcon(View v);
     public void onClickAppShortcut(View v);
+
+    @Deprecated
     public void onClickPagedViewIcon(View v);
     public void onClickWallpaperPicker(View v);
     public void onClickSettingsButton(View v);
diff --git a/src/com/android/launcher3/LauncherFiles.java b/src/com/android/launcher3/LauncherFiles.java
index c08cd0b..ec4e4f9 100644
--- a/src/com/android/launcher3/LauncherFiles.java
+++ b/src/com/android/launcher3/LauncherFiles.java
@@ -26,6 +26,8 @@
     public static final String WIDGET_PREVIEWS_DB = "widgetpreviews.db";
     public static final String APP_ICONS_DB = "app_icons.db";
 
+    public static final String ROTATION_PREF_FILE = "com.android.launcher3.rotation.prefs";
+
     public static final List<String> ALL_FILES = Collections.unmodifiableList(Arrays.asList(
             DEFAULT_WALLPAPER_THUMBNAIL,
             DEFAULT_WALLPAPER_THUMBNAIL_OLD,
@@ -35,7 +37,8 @@
             WALLPAPER_IMAGES_DB,
             WIDGET_PREVIEWS_DB,
             MANAGED_USER_PREFERENCES_KEY,
-            APP_ICONS_DB));
+            APP_ICONS_DB,
+            ROTATION_PREF_FILE));
 
     // TODO: Delete these files on upgrade
     public static final List<String> OBSOLETE_FILES = Collections.unmodifiableList(Arrays.asList(
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 224ebbf..776c2bd 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -275,7 +275,7 @@
     /**
      * Runs the specified runnable after the loader is complete
      */
-    private void runAfterBindCompletes(Runnable r) {
+    @Thunk void runAfterBindCompletes(Runnable r) {
         if (isLoadingWorkspace() || !mHasLoaderCompletedOnce) {
             synchronized (mBindCompleteRunnables) {
                 mBindCompleteRunnables.add(r);
@@ -3350,7 +3350,7 @@
      *
      * @see #loadAndBindWidgetsAndShortcuts
      */
-    private WidgetsModel createWidgetsModel(Context context, boolean refresh) {
+    @Thunk WidgetsModel createWidgetsModel(Context context, boolean refresh) {
         PackageManager packageManager = context.getPackageManager();
         final ArrayList<Object> widgetsAndShortcuts = new ArrayList<Object>();
         widgetsAndShortcuts.addAll(getWidgetProviders(context, refresh));
diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java
index 2751152..45070d1 100644
--- a/src/com/android/launcher3/LauncherProvider.java
+++ b/src/com/android/launcher3/LauncherProvider.java
@@ -88,7 +88,7 @@
     static final Uri CONTENT_APPWIDGET_RESET_URI =
             Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
 
-    private DatabaseHelper mOpenHelper;
+    @Thunk DatabaseHelper mOpenHelper;
 
     @Override
     public boolean onCreate() {
@@ -665,7 +665,7 @@
          * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
          */
-        private void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
+        @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
             db.beginTransaction();
             Cursor c = null;
             SQLiteStatement updateStmt = null;
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 9271e8b..18832c6 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -22,11 +22,13 @@
 import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Rect;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -45,9 +47,7 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.animation.Interpolator;
-
 import com.android.launcher3.util.Thunk;
-
 import java.util.ArrayList;
 
 /**
@@ -186,7 +186,7 @@
     // We use the min scale to determine how much to expand the actually PagedView measured
     // dimensions such that when we are zoomed out, the view is not clipped
     private static int REORDERING_DROP_REPOSITION_DURATION = 200;
-    private static int REORDERING_REORDER_REPOSITION_DURATION = 300;
+    @Thunk static int REORDERING_REORDER_REPOSITION_DURATION = 300;
     private static int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80;
 
     private float mMinScale = 1f;
@@ -956,7 +956,7 @@
         return 0;
     }
 
-    private void updateMaxScrollX() {
+    @Thunk void updateMaxScrollX() {
         int childCount = getChildCount();
         if (childCount > 0) {
             final int index = mIsRtl ? 0 : childCount - 1;
@@ -2322,6 +2322,7 @@
     private static final int ANIM_TAG_KEY = 100;
 
     /* Accessibility */
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     @Override
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
diff --git a/src/com/android/launcher3/SettingsActivity.java b/src/com/android/launcher3/SettingsActivity.java
new file mode 100644
index 0000000..a1da1b6
--- /dev/null
+++ b/src/com/android/launcher3/SettingsActivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+/**
+ * Settings activity for Launcher. Currently implements the following setting:
+ * LockToPortrait
+ */
+public class SettingsActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Display the fragment as the main content.
+        getFragmentManager().beginTransaction()
+                .replace(android.R.id.content, new LauncherSettingsFragment())
+                .commit();
+    }
+
+    /**
+     * This fragment shows the launcher preferences.
+     */
+    @SuppressWarnings("WeakerAccess")
+    public static class LauncherSettingsFragment extends PreferenceFragment {
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            getPreferenceManager().setSharedPreferencesMode(Context.MODE_PRIVATE);
+            getPreferenceManager().setSharedPreferencesName(LauncherFiles.ROTATION_PREF_FILE);
+            addPreferencesFromResource(R.xml.launcher_preferences);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/Stats.java b/src/com/android/launcher3/Stats.java
index 9d06f75..cb0e252 100644
--- a/src/com/android/launcher3/Stats.java
+++ b/src/com/android/launcher3/Stats.java
@@ -20,9 +20,63 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.Bundle;
 import android.util.Log;
+import android.view.View;
+import android.view.ViewParent;
 
 public class Stats {
+
+    /**
+     * Implemented by containers to provide a launch source for a given child.
+     */
+    public interface LaunchSourceProvider {
+        void fillInLaunchSourceData(Bundle sourceData);
+    }
+
+    /**
+     * Helpers to add the source to a launch intent.
+     */
+    public static class LaunchSourceUtils {
+        /**
+         * Create a default bundle for LaunchSourceProviders to fill in their data.
+         */
+        public static Bundle createSourceData() {
+            Bundle sourceData = new Bundle();
+            sourceData.putString(SOURCE_EXTRA_CONTAINER, CONTAINER_HOMESCREEN);
+            // Have default container/sub container pages
+            sourceData.putInt(SOURCE_EXTRA_CONTAINER_PAGE, 0);
+            sourceData.putInt(SOURCE_EXTRA_SUB_CONTAINER_PAGE, 0);
+            return sourceData;
+        }
+
+        /**
+         * Finds the next launch source provider in the parents of the view hierarchy and populates
+         * the source data from that provider.
+         */
+        public static void populateSourceDataFromAncestorProvider(View v, Bundle sourceData) {
+            if (v == null) {
+                return;
+            }
+
+            Stats.LaunchSourceProvider provider = null;
+            ViewParent parent = v.getParent();
+            while (parent != null && parent instanceof View) {
+                if (parent instanceof Stats.LaunchSourceProvider) {
+                    provider = (Stats.LaunchSourceProvider) parent;
+                    break;
+                }
+                parent = parent.getParent();
+            }
+
+            if (provider != null) {
+                provider.fillInLaunchSourceData(sourceData);
+            } else if (LauncherAppState.isDogfoodBuild()) {
+                throw new RuntimeException("Expected LaunchSourceProvider");
+            }
+        }
+    }
+
     private static final boolean DEBUG_BROADCASTS = false;
 
     public static final String ACTION_LAUNCH = "com.android.launcher3.action.LAUNCH";
@@ -31,6 +85,22 @@
     public static final String EXTRA_SCREEN = "screen";
     public static final String EXTRA_CELLX = "cellX";
     public static final String EXTRA_CELLY = "cellY";
+    public static final String EXTRA_SOURCE = "source";
+
+    public static final String SOURCE_EXTRA_CONTAINER = "container";
+    public static final String SOURCE_EXTRA_CONTAINER_PAGE = "container_page";
+    public static final String SOURCE_EXTRA_SUB_CONTAINER = "sub_container";
+    public static final String SOURCE_EXTRA_SUB_CONTAINER_PAGE = "sub_container_page";
+
+    public static final String CONTAINER_SEARCH_BOX = "search_box";
+    public static final String CONTAINER_ALL_APPS = "all_apps";
+    public static final String CONTAINER_HOMESCREEN = "homescreen"; // aka. Workspace
+    public static final String CONTAINER_HOTSEAT = "hotseat";
+
+    public static final String SUB_CONTAINER_FOLDER = "folder";
+    public static final String SUB_CONTAINER_ALL_APPS_A_Z = "a-z";
+    public static final String SUB_CONTAINER_ALL_APPS_PREDICTION = "prediction";
+    public static final String SUB_CONTAINER_ALL_APPS_SEARCH = "search";
 
     private final Launcher mLauncher;
     private final String mLaunchBroadcastPermission;
@@ -56,11 +126,7 @@
         }
     }
 
-    public void recordLaunch(Intent intent) {
-        recordLaunch(intent, null);
-    }
-
-    public void recordLaunch(Intent intent, ShortcutInfo shortcut) {
+    public void recordLaunch(View v, Intent intent, ShortcutInfo shortcut) {
         intent = new Intent(intent);
         intent.setSourceBounds(null);
 
@@ -72,6 +138,10 @@
                     .putExtra(EXTRA_CELLX, shortcut.cellX)
                     .putExtra(EXTRA_CELLY, shortcut.cellY);
         }
+
+        Bundle sourceExtras = LaunchSourceUtils.createSourceData();
+        LaunchSourceUtils.populateSourceDataFromAncestorProvider(v, sourceExtras);
+        broadcastIntent.putExtra(EXTRA_SOURCE, sourceExtras);
         mLauncher.sendBroadcast(broadcastIntent, mLaunchBroadcastPermission);
     }
 }
diff --git a/src/com/android/launcher3/StylusEventHelper.java b/src/com/android/launcher3/StylusEventHelper.java
new file mode 100644
index 0000000..da46e6a
--- /dev/null
+++ b/src/com/android/launcher3/StylusEventHelper.java
@@ -0,0 +1,84 @@
+package com.android.launcher3;
+
+import com.android.launcher3.Utilities;
+
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+/**
+ * Helper for identifying when a stylus touches a view while the primary stylus button is pressed.
+ * This can occur in {@value MotionEvent#ACTION_DOWN} or {@value MotionEvent#ACTION_MOVE}. On a
+ * stylus button press this performs the view's {@link View#performLongClick()} method, if the view
+ * is long clickable.
+ */
+public class StylusEventHelper {
+    private boolean mIsButtonPressed;
+    private View mView;
+
+    public StylusEventHelper(View view) {
+        mView = view;
+    }
+
+    /**
+     * Call this in onTouchEvent method of a view to identify a stylus button press and perform a
+     * long click (if the view is long clickable).
+     *
+     * @param event The event to check for a stylus button press.
+     * @return Whether a stylus event occurred and was handled.
+     */
+    public boolean checkAndPerformStylusEvent(MotionEvent event) {
+        final float slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();
+
+        if (!mView.isLongClickable()) {
+            // We don't do anything unless the view is long clickable.
+            return false;
+        }
+
+        final boolean stylusButtonPressed = isStylusButtonPressed(event);
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mIsButtonPressed = false;
+                if (stylusButtonPressed && mView.performLongClick()) {
+                    mIsButtonPressed = true;
+                    return true;
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (Utilities.pointInView(mView, event.getX(), event.getY(), slop)) {
+                    if (!mIsButtonPressed && stylusButtonPressed && mView.performLongClick()) {
+                        mIsButtonPressed = true;
+                        return true;
+                    } else if (mIsButtonPressed && !stylusButtonPressed) {
+                        mIsButtonPressed = false;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mIsButtonPressed = false;
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * Whether a stylus button press is occurring.
+     */
+    public boolean inStylusButtonPressed() {
+        return mIsButtonPressed;
+    }
+
+    /**
+     * Identifies if the provided {@link MotionEvent} is a stylus with the primary stylus button
+     * pressed.
+     *
+     * @param event The event to check.
+     * @return Whether a stylus button press occurred.
+     */
+    public static boolean isStylusButtonPressed(MotionEvent event) {
+        return event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
+                && event.isButtonPressed(MotionEvent.BUTTON_SECONDARY);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 256eba0..a9cbf69 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -25,6 +25,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -67,6 +68,7 @@
  * Various utilities shared amongst the Launcher's classes.
  */
 public final class Utilities {
+
     private static final String TAG = "Launcher.Utilities";
 
     private static int sIconWidth = -1;
@@ -93,6 +95,8 @@
     static final String FORCE_ENABLE_ROTATION_PROPERTY = "launcher_force_rotate";
     public static boolean sForceEnableRotation = isPropertyEnabled(FORCE_ENABLE_ROTATION_PROPERTY);
 
+    public static final String ALLOW_ROTATION_PREFERENCE_KEY = "pref_allowRotation";
+
     /**
      * Returns a FastBitmapDrawable with the icon, accurately sized.
      */
@@ -114,10 +118,15 @@
         return Log.isLoggable(propertyName, Log.VERBOSE);
     }
 
-    public static boolean isRotationEnabled(Context c) {
-        boolean enableRotation = sForceEnableRotation ||
-                c.getResources().getBoolean(R.bool.allow_rotation);
-        return enableRotation;
+    public static boolean isAllowRotationPrefEnabled(Context context) {
+        SharedPreferences sharedPrefs = context.getSharedPreferences(LauncherFiles.ROTATION_PREF_FILE,
+                Context.MODE_MULTI_PROCESS);
+        boolean allowRotationPref = sharedPrefs.getBoolean(ALLOW_ROTATION_PREFERENCE_KEY, false);
+        return sForceEnableRotation || allowRotationPref;
+    }
+
+    public static boolean isRotationAllowedForDevice(Context context) {
+        return sForceEnableRotation || context.getResources().getBoolean(R.bool.allow_rotation);
     }
 
     /**
diff --git a/src/com/android/launcher3/WidgetPreviewLoader.java b/src/com/android/launcher3/WidgetPreviewLoader.java
index e8cc486..a621771 100644
--- a/src/com/android/launcher3/WidgetPreviewLoader.java
+++ b/src/com/android/launcher3/WidgetPreviewLoader.java
@@ -56,7 +56,7 @@
      * Weak reference objects, do not prevent their referents from being made finalizable,
      * finalized, and then reclaimed.
      */
-    private Set<Bitmap> mUnusedBitmaps =
+    @Thunk Set<Bitmap> mUnusedBitmaps =
             Collections.newSetFromMap(new WeakHashMap<Bitmap, Boolean>());
 
     private final Context mContext;
@@ -67,7 +67,7 @@
     private final InvariantDeviceProfile mDeviceProfile;
 
     private final MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
-    private final Handler mWorkerHandler;
+    @Thunk final Handler mWorkerHandler;
 
     public WidgetPreviewLoader(Context context, InvariantDeviceProfile inv, IconCache iconCache) {
         mContext = context;
@@ -290,7 +290,7 @@
     /**
      * Reads the preview bitmap from the DB or null if the preview is not in the DB.
      */
-    private Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
+    @Thunk Bitmap readFromDb(WidgetCacheKey key, Bitmap recycle, PreviewLoadTask loadTask) {
         Cursor cursor = null;
         try {
             cursor = mDb.getReadableDatabase().query(
@@ -329,7 +329,7 @@
         return null;
     }
 
-    private Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle,
+    @Thunk Bitmap generatePreview(Launcher launcher, Object info, Bitmap recycle,
             int previewWidth, int previewHeight) {
         if (info instanceof LauncherAppWidgetProviderInfo) {
             return generateWidgetPreview(launcher, (LauncherAppWidgetProviderInfo) info,
@@ -512,7 +512,7 @@
     /**
      * @return an array of containing versionCode and lastUpdatedTime for the package.
      */
-    private long[] getPackageVersion(String packageName) {
+    @Thunk long[] getPackageVersion(String packageName) {
         synchronized (mPackageVersions) {
             long[] versions = mPackageVersions.get(packageName);
             if (versions == null) {
@@ -561,14 +561,13 @@
     }
 
     public class PreviewLoadTask extends AsyncTask<Void, Void, Bitmap> {
-
-        private final WidgetCacheKey mKey;
+        @Thunk final WidgetCacheKey mKey;
         private final Object mInfo;
         private final int mPreviewHeight;
         private final int mPreviewWidth;
         private final WidgetCell mCaller;
-        private long[] mVersions;
-        private Bitmap mBitmapToRecycle;
+        @Thunk long[] mVersions;
+        @Thunk Bitmap mBitmapToRecycle;
 
         PreviewLoadTask(WidgetCacheKey key, Object info, int previewWidth,
                 int previewHeight, WidgetCell caller) {
@@ -674,7 +673,7 @@
     private static final class WidgetCacheKey extends ComponentKey {
 
         // TODO: remove dependency on size
-        private final String size;
+        @Thunk final String size;
 
         public WidgetCacheKey(ComponentName componentName, UserHandleCompat user, String size) {
             super(componentName, user);
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 6d5affb..193a0af 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -28,6 +28,7 @@
 import android.appwidget.AppWidgetProviderInfo;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -42,6 +43,7 @@
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Parcelable;
@@ -86,7 +88,7 @@
 public class Workspace extends PagedView
         implements DropTarget, DragSource, DragScroller, View.OnTouchListener,
         DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener,
-        Insettable, UninstallSource, AccessibilityDragSource {
+        Insettable, UninstallSource, AccessibilityDragSource, Stats.LaunchSourceProvider {
     private static final String TAG = "Launcher.Workspace";
 
     private static boolean ENFORCE_DRAG_EVENT_ORDER = false;
@@ -4461,6 +4463,12 @@
         mLauncher.getDragLayer().getLocationInDragLayer(this, loc);
     }
 
+    @Override
+    public void fillInLaunchSourceData(Bundle sourceData) {
+        sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_HOMESCREEN);
+        sourceData.putInt(Stats.SOURCE_EXTRA_CONTAINER_PAGE, getCurrentPage());
+    }
+
     /**
      * Used as a workaround to ensure that the AppWidgetService receives the
      * PACKAGE_ADDED broadcast before updating widgets.
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 93cf8d0..3c49ccc 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -251,7 +251,7 @@
         return actions;
     }
 
-    private void performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
+    @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) {
         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams();
         CellLayout layout = (CellLayout) host.getParent().getParent();
         layout.markCellsAsUnoccupiedForView(host);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index d81f97f..9386500 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -26,6 +26,7 @@
 import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.InsetDrawable;
 import android.os.Build;
+import android.os.Bundle;
 import android.support.v7.widget.RecyclerView;
 import android.text.Editable;
 import android.text.TextWatcher;
@@ -41,6 +42,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.FrameLayout;
 import android.widget.TextView;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseContainerView;
 import com.android.launcher3.BubbleTextView;
@@ -54,15 +56,16 @@
 import com.android.launcher3.Insettable;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherTransitionable;
 import com.android.launcher3.R;
+import com.android.launcher3.Stats;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.allapps.AppSearchManager.AppSearchResultCallback;
 import com.android.launcher3.util.Thunk;
 
+import java.util.ArrayList;
 import java.util.List;
-import java.util.regex.Pattern;
 
 
 /**
@@ -171,7 +174,7 @@
         TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
         AlphabeticalAppsList.AdapterChangedCallback, AllAppsGridAdapter.PredictionBarSpacerCallbacks,
         View.OnTouchListener, View.OnClickListener, View.OnLongClickListener,
-        ViewTreeObserver.OnPreDrawListener {
+        ViewTreeObserver.OnPreDrawListener, AppSearchResultCallback, Stats.LaunchSourceProvider {
 
     public static final boolean GRID_MERGE_SECTIONS = true;
 
@@ -183,8 +186,6 @@
     private static final int FADE_OUT_DURATION = 100;
     private static final int SEARCH_TRANSLATION_X_DP = 18;
 
-    private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
-
     @Thunk Launcher mLauncher;
     @Thunk AlphabeticalAppsList mApps;
     private LayoutInflater mLayoutInflater;
@@ -192,14 +193,14 @@
     private RecyclerView.LayoutManager mLayoutManager;
     private RecyclerView.ItemDecoration mItemDecoration;
 
-    private FrameLayout mContentView;
+    @Thunk FrameLayout mContentView;
     @Thunk AllAppsRecyclerView mAppsRecyclerView;
-    private ViewGroup mPredictionBarView;
+    @Thunk ViewGroup mPredictionBarView;
     private View mHeaderView;
-    private View mSearchBarContainerView;
+    @Thunk View mSearchBarContainerView;
     private View mSearchButtonView;
     private View mDismissSearchButtonView;
-    private AllAppsSearchEditView mSearchBarEditView;
+    @Thunk AllAppsSearchEditView mSearchBarEditView;
 
     private HeaderElevationController mElevationController;
 
@@ -216,11 +217,13 @@
     private int mContainerInset;
     private int mPredictionBarHeight;
     private int mLastRecyclerViewScrollPos = -1;
-    private boolean mFocusPredictionBarOnFirstBind;
+    @Thunk boolean mFocusPredictionBarOnFirstBind;
 
     private CheckLongPressHelper mPredictionIconCheckForLongPress;
     private View mPredictionIconUnderTouch;
 
+    private AppSearchManager mSearchManager;
+
     public AllAppsContainerView(Context context) {
         this(context, null);
     }
@@ -231,7 +234,6 @@
 
     public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        LauncherAppState app = LauncherAppState.getInstance();
         Resources res = context.getResources();
 
         mLauncher = (Launcher) context;
@@ -258,6 +260,7 @@
         mContentMarginStart = mAdapter.getContentMarginStart();
 
         mApps.setAdapter(mAdapter);
+        mSearchManager = mApps.newSimpleAppSearchManager();
     }
 
     /**
@@ -281,6 +284,11 @@
         mApps.addApps(apps);
     }
 
+    public void setSearchManager(AppSearchManager searchManager) {
+        mSearchManager.cancel(true);
+        mSearchManager = searchManager;
+    }
+
     /**
      * Updates existing apps in the list
      */
@@ -664,45 +672,26 @@
     public void afterTextChanged(final Editable s) {
         String queryText = s.toString();
         if (queryText.isEmpty()) {
-            mApps.setFilter(null);
+            mSearchManager.cancel(true);
+            mApps.setOrderedFilter(null);
         } else {
             String formatStr = getResources().getString(R.string.all_apps_no_search_results);
             mAdapter.setEmptySearchText(String.format(formatStr, queryText));
 
-            // Do an intersection of the words in the query and each title, and filter out all the
-            // apps that don't match all of the words in the query.
-            final String queryTextLower = queryText.toLowerCase();
-            final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
-            mApps.setFilter(new AlphabeticalAppsList.Filter() {
-                @Override
-                public boolean retainApp(AppInfo info, String sectionName) {
-                    if (sectionName.toLowerCase().contains(queryTextLower)) {
-                        return true;
-                    }
-                    String title = info.title.toString();
-                    String[] words = SPLIT_PATTERN.split(title.toLowerCase());
-                    for (int qi = 0; qi < queryWords.length; qi++) {
-                        boolean foundMatch = false;
-                        for (int i = 0; i < words.length; i++) {
-                            if (words[i].startsWith(queryWords[qi])) {
-                                foundMatch = true;
-                                break;
-                            }
-                        }
-                        if (!foundMatch) {
-                            // If there is a word in the query that does not match any words in this
-                            // title, so skip it.
-                            return false;
-                        }
-                    }
-                    return true;
-                }
-            });
+            mSearchManager.cancel(false);
+            mSearchManager.doSearch(queryText, this);
         }
         scrollToTop();
     }
 
     @Override
+    public void onSearchResult(ArrayList<ComponentName> apps) {
+        if (apps != null) {
+            mApps.setOrderedFilter(apps);
+        }
+    }
+
+    @Override
     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
         if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
             // Skip the quick-launch if there isn't exactly one item
@@ -796,7 +785,6 @@
      * recycler view.
      */
     private boolean handleTouchEvent(MotionEvent ev) {
-        LauncherAppState app = LauncherAppState.getInstance();
         DeviceProfile grid = mLauncher.getDeviceProfile();
         int x = (int) ev.getX();
         int y = (int) ev.getY();
@@ -884,6 +872,15 @@
         return false;
     }
 
+    @Override
+    public void fillInLaunchSourceData(Bundle sourceData) {
+        // Since the other cases are caught by the AllAppsRecyclerView LaunchSourceProvider, we just
+        // handle the prediction bar icons here
+        sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
+        sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+                Stats.SUB_CONTAINER_ALL_APPS_PREDICTION);
+    }
+
     /**
      * Returns the predicted app in the prediction bar given a set of local coordinates.
      */
@@ -919,6 +916,8 @@
      * Shows the search field.
      */
     private void showSearchField() {
+        mSearchManager.connect();
+
         // Show the search bar and focus the search
         final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
                 getContext().getResources().getDisplayMetrics());
@@ -948,7 +947,9 @@
     /**
      * Hides the search field.
      */
-    private void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) {
+    @Thunk void hideSearchField(boolean animated, final boolean returnFocusToRecyclerView) {
+        mSearchManager.cancel(true);
+
         final boolean resetTextField = mSearchBarEditView.getText().toString().length() > 0;
         final int translationX = Utilities.pxFromDp(SEARCH_TRANSLATION_X_DP,
                 getContext().getResources().getDisplayMetrics());
@@ -966,7 +967,7 @@
                             if (resetTextField) {
                                 mSearchBarEditView.setText("");
                             }
-                            mApps.setFilter(null);
+                            mApps.setOrderedFilter(null);
                             if (returnFocusToRecyclerView) {
                                 mAppsRecyclerView.requestFocus();
                             }
@@ -983,7 +984,7 @@
             if (resetTextField) {
                 mSearchBarEditView.setText("");
             }
-            mApps.setFilter(null);
+            mApps.setOrderedFilter(null);
             mSearchButtonView.setAlpha(1f);
             mSearchButtonView.setTranslationX(0f);
             if (returnFocusToRecyclerView) {
@@ -1011,7 +1012,7 @@
     /**
      * Returns an input method manager.
      */
-    private InputMethodManager getInputMethodManager() {
+    @Thunk InputMethodManager getInputMethodManager() {
         return (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
     }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index e010270..307d940 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -288,7 +288,7 @@
     private GridLayoutManager mGridLayoutMgr;
     private GridSpanSizer mGridSizer;
     private GridItemDecoration mItemDecoration;
-    private PredictionBarSpacerCallbacks mPredictionBarCb;
+    @Thunk PredictionBarSpacerCallbacks mPredictionBarCb;
     private View.OnTouchListener mTouchListener;
     private View.OnClickListener mIconClickListener;
     private View.OnLongClickListener mIconLongClickListener;
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index cc5add3..e1b5d91 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -18,14 +18,15 @@
 import android.content.Context;
 import android.graphics.Rect;
 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.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.Stats;
 import com.android.launcher3.Utilities;
 
 import java.util.List;
@@ -33,7 +34,8 @@
 /**
  * A RecyclerView with custom fast scroll support for the all apps view.
  */
-public class AllAppsRecyclerView extends BaseRecyclerView {
+public class AllAppsRecyclerView extends BaseRecyclerView
+        implements Stats.LaunchSourceProvider {
 
     private AlphabeticalAppsList mApps;
     private int mNumAppsPerRow;
@@ -125,6 +127,18 @@
         addOnItemTouchListener(this);
     }
 
+    @Override
+    public void fillInLaunchSourceData(Bundle sourceData) {
+        sourceData.putString(Stats.SOURCE_EXTRA_CONTAINER, Stats.CONTAINER_ALL_APPS);
+        if (mApps.hasFilter()) {
+            sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+                    Stats.SUB_CONTAINER_ALL_APPS_SEARCH);
+        } else {
+            sourceData.putString(Stats.SOURCE_EXTRA_SUB_CONTAINER,
+                    Stats.SUB_CONTAINER_ALL_APPS_A_Z);
+        }
+    }
+
     /**
      * Maps the touch (from 0..1) to the adapter position that should be visible.
      */
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 3d1503d..e284f77 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -1,15 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package com.android.launcher3.allapps;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
+import com.android.launcher3.model.AbstractUserComparator;
 import com.android.launcher3.model.AppNameComparator;
+import com.android.launcher3.util.Thunk;
 
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -25,6 +46,7 @@
 
     public static final String TAG = "AlphabeticalAppsList";
     private static final boolean DEBUG = false;
+    private static final boolean DEBUG_PREDICTIONS = false;
 
     /**
      * Info about a section in the alphabetic list
@@ -113,13 +135,6 @@
     }
 
     /**
-     * A filter interface to limit the set of applications in the apps list.
-     */
-    public interface Filter {
-        boolean retainApp(AppInfo info, String sectionName);
-    }
-
-    /**
      * Callback to notify when the set of adapter items have changed.
      */
     public interface AdapterChangedCallback {
@@ -130,46 +145,63 @@
      * Common interface for different merging strategies.
      */
     private interface MergeAlgorithm {
-        boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount);
+        boolean continueMerging(SectionInfo section, SectionInfo withSection,
+                int sectionAppCount, int numAppsPerRow, int mergeCount);
     }
 
     /**
-     * The logic we use to merge sections on tablets.
+     * The logic we use to merge sections on tablets.  Currently, we don't show section names on
+     * tablet layouts, so just merge all the sections indiscriminately.
      */
-    private static class TabletMergeAlgorithm implements MergeAlgorithm {
+    @Thunk static class TabletMergeAlgorithm implements MergeAlgorithm {
 
         @Override
-        public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) {
+        public boolean continueMerging(SectionInfo section, SectionInfo withSection,
+                int sectionAppCount, int numAppsPerRow, int mergeCount) {
             // Merge EVERYTHING
             return true;
         }
     }
 
     /**
-     * The logic we use to merge sections on phones.
+     * The logic we use to merge sections on phones.  We only merge sections when their final row
+     * contains less than a certain number of icons, and stop at a specified max number of merges.
+     * In addition, we will try and not merge sections that identify apps from different scripts.
      */
     private static class PhoneMergeAlgorithm implements MergeAlgorithm {
 
         private int mMinAppsPerRow;
         private int mMinRowsInMergedSection;
         private int mMaxAllowableMerges;
+        private CharsetEncoder mAsciiEncoder;
 
         public PhoneMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) {
             mMinAppsPerRow = minAppsPerRow;
             mMinRowsInMergedSection = minRowsInMergedSection;
             mMaxAllowableMerges = maxNumMerges;
+            mAsciiEncoder = Charset.forName("US-ASCII").newEncoder();
         }
 
         @Override
-        public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) {
+        public boolean continueMerging(SectionInfo section, SectionInfo withSection,
+                int sectionAppCount, int numAppsPerRow, int mergeCount) {
             // Continue merging if the number of hanging apps on the final row is less than some
             // fixed number (ragged), the merged rows has yet to exceed some minimum row count,
             // and while the number of merged sections is less than some fixed number of merges
             int rows = sectionAppCount / numAppsPerRow;
             int cols = sectionAppCount % numAppsPerRow;
+
+            // Ensure that we do not merge across scripts, currently we only allow for english and
+            // native scripts so we can test if both can just be ascii encoded
+            boolean isCrossScript = false;
+            if (section.firstAppItem != null && withSection.firstAppItem != null) {
+                isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) !=
+                        mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName);
+            }
             return (0 < cols && cols < mMinAppsPerRow) &&
                     rows < mMinRowsInMergedSection &&
-                    mergeCount < mMaxAllowableMerges;
+                    mergeCount < mMaxAllowableMerges &&
+                    !isCrossScript;
         }
     }
 
@@ -179,7 +211,7 @@
     private Launcher mLauncher;
 
     // The set of apps from the system not including predictions
-    private List<AppInfo> mApps = new ArrayList<>();
+    private final List<AppInfo> mApps = new ArrayList<>();
     // The set of filtered apps with the current filter
     private List<AppInfo> mFilteredApps = new ArrayList<>();
     // The current set of adapter items
@@ -192,9 +224,10 @@
     private List<ComponentName> mPredictedAppComponents = new ArrayList<>();
     // The set of predicted apps resolved from the component names and the current set of apps
     private List<AppInfo> mPredictedApps = new ArrayList<>();
+    // The of ordered component names as a result of a search query
+    private ArrayList<ComponentName> mSearchResults;
     private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
     private RecyclerView.Adapter mAdapter;
-    private Filter mFilter;
     private AlphabeticIndexCompat mIndexer;
     private AppNameComparator mAppNameComparator;
     private MergeAlgorithm mMergeAlgorithm;
@@ -216,6 +249,10 @@
         mAdapterChangedCallback = cb;
     }
 
+    public SimpleAppSearchManagerImpl newSimpleAppSearchManager() {
+        return new SimpleAppSearchManagerImpl(mApps);
+    }
+
     /**
      * Sets the number of apps per row.  Used only for AppsContainerView.SECTIONED_GRID_COALESCED.
      */
@@ -274,22 +311,22 @@
      * Returns whether there are is a filter set.
      */
     public boolean hasFilter() {
-        return (mFilter != null);
+        return (mSearchResults != null);
     }
 
     /**
      * Returns whether there are no filtered results.
      */
     public boolean hasNoFilteredResults() {
-        return (mFilter != null) && mFilteredApps.isEmpty();
+        return (mSearchResults != null) && mFilteredApps.isEmpty();
     }
 
     /**
-     * Sets the current filter for this list of apps.
+     * Sets the sorted list of filtered components.
      */
-    public void setFilter(Filter f) {
-        if (mFilter != f) {
-            mFilter = f;
+    public void setOrderedFilter(ArrayList<ComponentName> f) {
+        if (mSearchResults != f) {
+            mSearchResults = f;
             updateAdapterItems();
         }
     }
@@ -409,7 +446,9 @@
             for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
                 allApps.addAll(entry.getValue());
             }
-            mApps = allApps;
+
+            mApps.clear();
+            mApps.addAll(allApps);
         } else {
             // Just compute the section headers for use below
             for (AppInfo info : mApps) {
@@ -439,6 +478,15 @@
         mAdapterItems.clear();
         mSections.clear();
 
+        if (DEBUG_PREDICTIONS) {
+            if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
+                mPredictedAppComponents.add(mApps.get(0).componentName);
+                mPredictedAppComponents.add(mApps.get(0).componentName);
+                mPredictedAppComponents.add(mApps.get(0).componentName);
+                mPredictedAppComponents.add(mApps.get(0).componentName);
+            }
+        }
+
         // Process the predicted app components
         mPredictedApps.clear();
         if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
@@ -464,16 +512,12 @@
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
         // ordered set of sections
-        int numApps = mApps.size();
+        List<AppInfo> apps = getFiltersAppInfos();
+        int numApps = apps.size();
         for (int i = 0; i < numApps; i++) {
-            AppInfo info = mApps.get(i);
+            AppInfo info = apps.get(i);
             String sectionName = getAndUpdateCachedSectionName(info.title);
 
-            // Check if we want to retain this app
-            if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
-                continue;
-            }
-
             // Create a new section if the section names do not match
             if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
                 lastSectionName = sectionName;
@@ -514,6 +558,41 @@
         }
     }
 
+    private List<AppInfo> getFiltersAppInfos() {
+        if (mSearchResults == null) {
+            return mApps;
+        }
+
+        int total = mSearchResults.size();
+        final HashMap<ComponentName, Integer> sortOrder = new HashMap<>(total);
+        for (int i = 0; i < total; i++) {
+            sortOrder.put(mSearchResults.get(i), i);
+        }
+
+        ArrayList<AppInfo> result = new ArrayList<>();
+        for (AppInfo info : mApps) {
+            if (sortOrder.containsKey(info.componentName)) {
+                result.add(info);
+            }
+        }
+
+        Collections.sort(result, new AbstractUserComparator<AppInfo>(
+                LauncherAppState.getInstance().getContext()) {
+
+            @Override
+            public int compare(AppInfo lhs, AppInfo rhs) {
+                Integer indexA = sortOrder.get(lhs.componentName);
+                int result = indexA.compareTo(sortOrder.get(rhs.componentName));
+                if (result == 0) {
+                    return super.compare(lhs, rhs);
+                } else {
+                    return result;
+                }
+            }
+        });
+        return result;
+    }
+
     /**
      * Merges multiple sections to reduce visual raggedness.
      */
@@ -521,14 +600,15 @@
         // Go through each section and try and merge some of the sections
         if (AllAppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) {
             int sectionAppCount = 0;
-            for (int i = 0; i < mSections.size(); i++) {
+            for (int i = 0; i < mSections.size() - 1; i++) {
                 SectionInfo section = mSections.get(i);
                 sectionAppCount = section.numApps;
                 int mergeCount = 1;
 
                 // Merge rows based on the current strategy
-                while (mMergeAlgorithm.continueMerging(sectionAppCount, mNumAppsPerRow, mergeCount) &&
-                        (i + 1) < mSections.size()) {
+                while (i < (mSections.size() - 1) &&
+                        mMergeAlgorithm.continueMerging(section, mSections.get(i + 1),
+                                sectionAppCount, mNumAppsPerRow, mergeCount)) {
                     SectionInfo nextSection = mSections.remove(i + 1);
 
                     // Remove the next section break
diff --git a/src/com/android/launcher3/allapps/AppSearchManager.java b/src/com/android/launcher3/allapps/AppSearchManager.java
new file mode 100644
index 0000000..b6aa223
--- /dev/null
+++ b/src/com/android/launcher3/allapps/AppSearchManager.java
@@ -0,0 +1,59 @@
+/*
+ * 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.content.ComponentName;
+
+import java.util.ArrayList;
+
+/**
+ * Interface for handling app search.
+ */
+public interface AppSearchManager {
+
+    /**
+     * Called when the search is about to be used. This method is optional for making a query but
+     * calling this appropriately can improve the initial response time.
+     */
+    void connect();
+
+    /**
+     * Cancels all pending search requests.
+     *
+     * @param interruptActiveRequests if true, any active requests which are already executing will
+     * be invalidated, and the corresponding results will not be sent. The client should usually
+     * set this to true, before beginning a new search session.
+     */
+    void cancel(boolean interruptActiveRequests);
+
+    /**
+     * Performs a search
+     */
+    void doSearch(String query, AppSearchResultCallback callback);
+
+    /**
+     * Callback for getting search results.
+     */
+    public interface AppSearchResultCallback {
+
+        /**
+         * Called when the search is complete.
+         *
+         * @param apps sorted list of matching components or null if in case of failure.
+         */
+        void onSearchResult(ArrayList<ComponentName> apps);
+    }
+}
diff --git a/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java b/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java
new file mode 100644
index 0000000..e8a31b5
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SimpleAppSearchManagerImpl.java
@@ -0,0 +1,98 @@
+/*
+ * 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.content.ComponentName;
+import android.os.Handler;
+
+import com.android.launcher3.AppInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link AppSearchManager} which does label matching on the UI thread.
+ */
+public class SimpleAppSearchManagerImpl implements AppSearchManager {
+
+    private static final Pattern SPLIT_PATTERN = Pattern.compile("[\\s|\\p{javaSpaceChar}]+");
+
+    private final List<AppInfo> mApps;
+    private final Handler mResultHandler;
+
+    public SimpleAppSearchManagerImpl(List<AppInfo> apps) {
+        mApps = apps;
+        mResultHandler = new Handler();
+    }
+
+    @Override
+    public void connect() {
+        // No op
+    }
+
+    @Override
+    public void cancel(boolean interruptActiveRequests) {
+        if (interruptActiveRequests) {
+            mResultHandler.removeCallbacksAndMessages(null);
+        }
+    }
+
+    @Override
+    public void doSearch(String query, final AppSearchResultCallback callback) {
+        // Do an intersection of the words in the query and each title, and filter out all the
+        // apps that don't match all of the words in the query.
+        final String queryTextLower = query.toLowerCase();
+        final String[] queryWords = SPLIT_PATTERN.split(queryTextLower);
+        final ArrayList<ComponentName> result = new ArrayList<ComponentName>();
+        int total = mApps.size();
+
+        for (int i = 0; i < total; i++) {
+            AppInfo info = mApps.get(i);
+            if (!result.contains(info.componentName) && matches(info, queryWords)) {
+                result.add(info.componentName);
+            }
+        }
+        mResultHandler.post(new Runnable() {
+
+            @Override
+            public void run() {
+                callback.onSearchResult(result);
+            }
+        });
+    }
+
+    private boolean matches(AppInfo info, String[] queryWords) {
+        String title = info.title.toString();
+        String[] words = SPLIT_PATTERN.split(title.toLowerCase());
+        for (int qi = 0; qi < queryWords.length; qi++) {
+            boolean foundMatch = false;
+            for (int i = 0; i < words.length; i++) {
+                if (words[i].startsWith(queryWords[qi])) {
+                    foundMatch = true;
+                    break;
+                }
+            }
+            if (!foundMatch) {
+                // If there is a word in the query that does not match any words in this
+                // title, so skip it.
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/src/com/android/launcher3/model/AbstractUserComparator.java b/src/com/android/launcher3/model/AbstractUserComparator.java
new file mode 100644
index 0000000..cf47ce6
--- /dev/null
+++ b/src/com/android/launcher3/model/AbstractUserComparator.java
@@ -0,0 +1,67 @@
+/*
+ * 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.model;
+
+import android.content.Context;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.compat.UserManagerCompat;
+
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * A comparator to arrange items based on user profiles.
+ */
+public abstract class AbstractUserComparator<T extends ItemInfo> implements Comparator<T> {
+
+    private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
+    private final UserManagerCompat mUserManager;
+    private final UserHandleCompat mMyUser;
+
+    public AbstractUserComparator(Context context) {
+        mUserManager = UserManagerCompat.getInstance(context);
+        mMyUser = UserHandleCompat.myUserHandle();
+    }
+
+    @Override
+    public int compare(T lhs, T rhs) {
+        if (mMyUser.equals(lhs.user)) {
+            return -1;
+        } else {
+            Long aUserSerial = getAndCacheUserSerial(lhs.user);
+            Long bUserSerial = getAndCacheUserSerial(rhs.user);
+            return aUserSerial.compareTo(bUserSerial);
+        }
+    }
+
+    /**
+     * Returns the user serial for this user, using a cached serial if possible.
+     */
+    private Long getAndCacheUserSerial(UserHandleCompat user) {
+        Long userSerial = mUserSerialCache.get(user);
+        if (userSerial == null) {
+            userSerial = mUserManager.getSerialNumberForUser(user);
+            mUserSerialCache.put(user, userSerial);
+        }
+        return userSerial;
+    }
+
+    public void clearUserCache() {
+        mUserSerialCache.clear();
+    }
+}
diff --git a/src/com/android/launcher3/model/AppNameComparator.java b/src/com/android/launcher3/model/AppNameComparator.java
index 706f751..cd45d2c 100644
--- a/src/com/android/launcher3/model/AppNameComparator.java
+++ b/src/com/android/launcher3/model/AppNameComparator.java
@@ -1,14 +1,28 @@
+/*
+ * 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.model;
 
 import android.content.Context;
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.ItemInfo;
-import com.android.launcher3.compat.UserHandleCompat;
-import com.android.launcher3.compat.UserManagerCompat;
+import com.android.launcher3.util.Thunk;
+
 import java.text.Collator;
 import java.util.Comparator;
-import java.util.HashMap;
 
 /**
  * Class to manage access to an app name comparator.
@@ -16,17 +30,15 @@
  * Used to sort application name in all apps view and widget tray view.
  */
 public class AppNameComparator {
-    private final UserManagerCompat mUserManager;
     private final Collator mCollator;
-    private final Comparator<ItemInfo> mAppInfoComparator;
+    private final AbstractUserComparator<ItemInfo> mAppInfoComparator;
     private final Comparator<String> mSectionNameComparator;
-    private HashMap<UserHandleCompat, Long> mUserSerialCache = new HashMap<>();
 
     public AppNameComparator(Context context) {
         mCollator = Collator.getInstance();
-        mUserManager = UserManagerCompat.getInstance(context);
-        mAppInfoComparator = new Comparator<ItemInfo>() {
+        mAppInfoComparator = new AbstractUserComparator<ItemInfo>(context) {
 
+            @Override
             public final int compare(ItemInfo a, ItemInfo b) {
                 // Order by the title in the current locale
                 int result = compareTitles(a.title.toString(), b.title.toString());
@@ -38,13 +50,7 @@
                     if (result == 0) {
                         // If the two apps are the same component, then prioritize by the order that
                         // the app user was created (prioritizing the main user's apps)
-                        if (UserHandleCompat.myUserHandle().equals(a.user)) {
-                            return -1;
-                        } else {
-                            Long aUserSerial = getAndCacheUserSerial(a.user);
-                            Long bUserSerial = getAndCacheUserSerial(b.user);
-                            return aUserSerial.compareTo(bUserSerial);
-                        }
+                        return super.compare(a, b);
                     }
                 }
                 return result;
@@ -63,7 +69,7 @@
      */
     public Comparator<ItemInfo> getAppInfoComparator() {
         // Clear the user serial cache so that we get serials as needed in the comparator
-        mUserSerialCache.clear();
+        mAppInfoComparator.clearUserCache();
         return mAppInfoComparator;
     }
 
@@ -77,7 +83,7 @@
     /**
      * Compares two titles with the same return value semantics as Comparator.
      */
-    private int compareTitles(String titleA, String titleB) {
+    @Thunk int compareTitles(String titleA, String titleB) {
         // Ensure that we de-prioritize any titles that don't start with a linguistic letter or digit
         boolean aStartsWithLetter = Character.isLetterOrDigit(titleA.codePointAt(0));
         boolean bStartsWithLetter = Character.isLetterOrDigit(titleB.codePointAt(0));
@@ -90,16 +96,4 @@
         // Order by the title in the current locale
         return mCollator.compare(titleA, titleB);
     }
-
-    /**
-     * Returns the user serial for this user, using a cached serial if possible.
-     */
-    private Long getAndCacheUserSerial(UserHandleCompat user) {
-        Long userSerial = mUserSerialCache.get(user);
-        if (userSerial == null) {
-            userSerial = mUserManager.getSerialNumberForUser(user);
-            mUserSerialCache.put(user, userSerial);
-        }
-        return userSerial;
-    }
 }
diff --git a/src/com/android/launcher3/util/LongArrayMap.java b/src/com/android/launcher3/util/LongArrayMap.java
index e3c96cd..a337e85 100644
--- a/src/com/android/launcher3/util/LongArrayMap.java
+++ b/src/com/android/launcher3/util/LongArrayMap.java
@@ -43,7 +43,7 @@
         return new ValueIterator();
     }
 
-    private class ValueIterator implements Iterator<E> {
+    @Thunk class ValueIterator implements Iterator<E> {
 
         private int mNextIndex = 0;
 
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 3ec1645..2714f51 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -23,6 +23,7 @@
 import android.graphics.Bitmap;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnLayoutChangeListener;
 import android.widget.LinearLayout;
@@ -35,6 +36,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.R;
+import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.WidgetPreviewLoader.PreviewLoadRequest;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
@@ -73,6 +75,7 @@
 
     private WidgetPreviewLoader mWidgetPreviewLoader;
     private PreviewLoadRequest mActiveRequest;
+    private StylusEventHelper mStylusEventHelper;
 
     private Launcher mLauncher;
 
@@ -89,6 +92,7 @@
 
         final Resources r = context.getResources();
         mLauncher = (Launcher) context;
+        mStylusEventHelper = new StylusEventHelper(this);
 
         mDimensionsFormatString = r.getString(R.string.widget_dims_format);
         setContainerWidth();
@@ -202,6 +206,15 @@
         return Math.min(size[0], info.spanX * cellWidth);
     }
 
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        boolean handled = super.onTouchEvent(ev);
+        if (mStylusEventHelper.checkAndPerformStylusEvent(ev)) {
+            return true;
+        }
+        return handled;
+    }
+
     /**
      * Helper method to get the string info of the tag.
      */
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index d654550..8875879 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -15,6 +15,7 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.util.Thunk;
 
 public class WidgetHostViewLoader {
 
@@ -38,7 +39,7 @@
     PendingAddWidgetInfo mCreateWidgetInfo = null;
 
     // TODO: technically, this class should not have to know the existence of the launcher.
-    private Launcher mLauncher;
+    @Thunk Launcher mLauncher;
     private Handler mHandler;
 
     public WidgetHostViewLoader(Launcher launcher) {
@@ -188,7 +189,7 @@
         return options;
     }
 
-    private void setState(int state) {
+    @Thunk void setState(int state) {
         if (DEBUG) {
             Log.d(TAG, String.format("     state [%d -> %d]", mState, state));
         }
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index 11c2107..8d04be5 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.WidgetPreviewLoader;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.util.Thunk;
 
 /**
  * The widgets list view container.
@@ -60,7 +61,7 @@
     private static final int PRELOAD_SCREEN_HEIGHT_MULTIPLE = 1;
 
     /* Global instances that are used inside this container. */
-    private Launcher mLauncher;
+    @Thunk Launcher mLauncher;
     private DragController mDragController;
     private IconCache mIconCache;