Merge "Extend accessibility timeout indefinitely before overview screenshot" into tm-qpr-dev
diff --git a/protos/view_capture.proto b/protos/view_capture.proto
new file mode 100644
index 0000000..98574dd
--- /dev/null
+++ b/protos/view_capture.proto
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto2";
+
+package com.android.launcher3.view;
+
+option java_outer_classname = "ViewCaptureData";
+
+message ExportedData {
+
+  repeated FrameData frameData = 1;
+}
+
+message FrameData {
+  optional int64 timestamp = 1;
+  optional ViewNode node = 2;
+}
+
+message ViewNode {
+  optional string classname = 1;
+  optional string id = 2;
+  optional int32 left = 3;
+  optional int32 top = 4;
+  optional int32 width = 5;
+  optional int32 height = 6;
+  optional int32 scrollX = 7;
+  optional int32 scrollY = 8;
+
+  optional float translationX = 9;
+  optional float translationY = 10;
+  optional float scaleX = 11 [default = 1];
+  optional float scaleY = 12 [default = 1];
+  optional float alpha = 13 [default = 1];
+
+  optional bool willNotDraw = 14;
+  optional bool clipChildren = 15;
+  optional int32 visibility = 16;
+
+  repeated ViewNode children = 17;
+}
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 0fd3c4a..17a48a7 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -66,7 +66,6 @@
     <dimen name="quickstep_fling_threshold_speed">0.5dp</dimen>
 
     <!-- Launcher app transition -->
-    <item name="content_scale" format="float" type="dimen">0.97</item>
     <dimen name="closing_window_trans_y">115dp</dimen>
 
     <dimen name="quick_switch_scaling_scroll_threshold">100dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index b20752d..bb79c1b 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -200,7 +200,6 @@
 
     final Handler mHandler;
 
-    private final float mContentScale;
     private final float mClosingWindowTransY;
     private final float mMaxShadowRadius;
 
@@ -245,7 +244,6 @@
         mBackAnimationController = new LauncherBackAnimationController(mLauncher, this);
 
         Resources res = mLauncher.getResources();
-        mContentScale = res.getFloat(R.dimen.content_scale);
         mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y);
         mMaxShadowRadius = res.getDimensionPixelSize(R.dimen.max_shadow_radius);
 
@@ -483,8 +481,8 @@
                 : new float[]{0, 1};
 
         float[] scales = isAppOpening
-                ? new float[]{1, mContentScale}
-                : new float[]{mContentScale, 1};
+                ? new float[]{1, mDeviceProfile.workspaceContentScale}
+                : new float[]{mDeviceProfile.workspaceContentScale, 1};
 
         // Pause expensive view updates as they can lead to layer thrashing and skipped frames.
         mLauncher.pauseExpensiveViewUpdates();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index a74774c..9f2efc4 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -31,8 +31,6 @@
  */
 public class AllAppsState extends LauncherState {
 
-    private static final float WORKSPACE_SCALE_FACTOR = 0.97f;
-
     private static final int STATE_FLAGS =
             FLAG_WORKSPACE_INACCESSIBLE | FLAG_CLOSE_POPUPS | FLAG_HOTSEAT_INACCESSIBLE;
 
@@ -60,7 +58,8 @@
 
     @Override
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(WORKSPACE_SCALE_FACTOR, NO_OFFSET, NO_OFFSET);
+        return new ScaleAndTranslation(launcher.getDeviceProfile().workspaceContentScale, NO_OFFSET,
+                NO_OFFSET);
     }
 
     @Override
@@ -71,7 +70,7 @@
             ScaleAndTranslation overviewScaleAndTranslation = LauncherState.OVERVIEW
                     .getWorkspaceScaleAndTranslation(launcher);
             return new ScaleAndTranslation(
-                    WORKSPACE_SCALE_FACTOR,
+                    launcher.getDeviceProfile().workspaceContentScale,
                     overviewScaleAndTranslation.translationX,
                     overviewScaleAndTranslation.translationY);
         }
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index 24028fa..098c9b0 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -41,7 +41,7 @@
     </com.android.launcher3.widget.picker.WidgetPagedView>
 
     <!-- SearchAndRecommendationsView contains the tab layout as well -->
-    <com.android.launcher3.widget.picker.SearchAndRecommendationsView
+    <com.android.launcher3.views.StickyHeaderLayout
         android:id="@+id/search_and_recommendations_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -68,7 +68,7 @@
             android:background="?android:attr/colorBackground"
             android:paddingBottom="8dp"
             android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
-            android:clipToPadding="false">
+            launcher:layout_sticky="true">
             <include layout="@layout/widgets_search_bar" />
         </FrameLayout>
 
@@ -92,7 +92,8 @@
             android:paddingLeft="@dimen/widget_tabs_horizontal_padding"
             android:paddingRight="@dimen/widget_tabs_horizontal_padding"
             android:background="?android:attr/colorBackground"
-            style="@style/TextHeadline">
+            style="@style/TextHeadline"
+            launcher:layout_sticky="true">
 
             <Button
                 android:id="@+id/tab_personal"
@@ -121,5 +122,5 @@
                 style="?android:attr/borderlessButtonStyle" />
         </com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
 
-    </com.android.launcher3.widget.picker.SearchAndRecommendationsView>
+    </com.android.launcher3.views.StickyHeaderLayout>
 </merge>
\ No newline at end of file
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index f4b5a0a..9da3e87 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -13,7 +13,8 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:launcher="http://schemas.android.com/apk/res-auto" >
     <com.android.launcher3.widget.picker.WidgetsRecyclerView
         android:id="@+id/primary_widgets_list_view"
         android:layout_below="@id/collapse_handle"
@@ -23,7 +24,7 @@
         android:clipToPadding="false" />
 
     <!-- SearchAndRecommendationsView without the tab layout as well -->
-    <com.android.launcher3.widget.picker.SearchAndRecommendationsView
+    <com.android.launcher3.views.StickyHeaderLayout
         android:id="@+id/search_and_recommendations_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -50,7 +51,8 @@
             android:background="?android:attr/colorBackground"
             android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
             android:paddingBottom="8dp"
-            android:clipToPadding="false">
+            android:clipToPadding="false"
+            launcher:layout_sticky="true" >
             <include layout="@layout/widgets_search_bar" />
         </FrameLayout>
 
@@ -63,6 +65,6 @@
             android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
             android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
             android:visibility="gone" />
-    </com.android.launcher3.widget.picker.SearchAndRecommendationsView>
+    </com.android.launcher3.views.StickyHeaderLayout>
 
 </merge>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index af8d8eb..e5b588c 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -136,6 +136,10 @@
         <attr name="layout_ignoreInsets" format="boolean" />
     </declare-styleable>
 
+    <declare-styleable name="StickyScroller_Layout">
+        <attr name="layout_sticky" format="boolean" />
+    </declare-styleable>
+
     <declare-styleable name="GridDisplayOption">
         <attr name="name" format="string" />
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 5e33de8..a3a30e1 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -413,4 +413,7 @@
     <dimen name="bottom_sheet_handle_height">4dp</dimen>
     <dimen name="bottom_sheet_handle_margin">16dp</dimen>
     <dimen name="bottom_sheet_handle_corner_radius">2dp</dimen>
+
+    <!-- State transition -->
+    <item name="workspace_content_scale" format="float" type="dimen">0.97</item>
 </resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 1bc269d..673ab54 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -107,6 +107,7 @@
     public Rect cellLayoutPaddingPx = new Rect();
 
     public final int edgeMarginPx;
+    public final float workspaceContentScale;
     public float workspaceSpringLoadShrunkTop;
     public float workspaceSpringLoadShrunkBottom;
     public final int workspaceSpringLoadedBottomSpace;
@@ -298,6 +299,7 @@
         }
 
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+        workspaceContentScale = res.getFloat(R.dimen.workspace_content_scale);
 
         desiredWorkspaceHorizontalMarginPx = getHorizontalMarginPx(inv, res);
         desiredWorkspaceHorizontalMarginOriginalPx = desiredWorkspaceHorizontalMarginPx;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5081f4f..9c62251 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -30,7 +30,10 @@
 import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
 import static com.android.launcher3.AbstractFloatingView.TYPE_SNACKBAR;
 import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
+import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
+import static com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY;
 import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.FLAG_MULTI_PAGE;
@@ -96,6 +99,7 @@
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.text.method.TextKeyListener;
+import android.util.FloatProperty;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.KeyEvent;
@@ -194,6 +198,7 @@
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.UiThreadHelper;
+import com.android.launcher3.util.ViewCapture;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.FloatingIconView;
@@ -301,6 +306,11 @@
     public static final int DISPLAY_WORKSPACE_TRACE_COOKIE = 0;
     public static final int DISPLAY_ALL_APPS_TRACE_COOKIE = 1;
 
+    private static final FloatProperty<Workspace<?>> WORKSPACE_WIDGET_SCALE =
+            WORKSPACE_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WIDGET_TRANSITION);
+    private static final FloatProperty<Hotseat> HOTSEAT_WIDGET_SCALE =
+            HOTSEAT_SCALE_PROPERTY_FACTORY.get(SCALE_INDEX_WIDGET_TRANSITION);
+
     private Configuration mOldConfig;
 
     @Thunk
@@ -388,6 +398,7 @@
     private LauncherState mPrevLauncherState;
 
     private StringCache mStringCache;
+    private ViewCapture mViewCapture;
 
     @Override
     @TargetApi(Build.VERSION_CODES.S)
@@ -1478,6 +1489,14 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
         mOverlayManager.onAttachedToWindow();
+        if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) {
+            View root = getDragLayer().getRootView();
+            if (mViewCapture != null) {
+                root.getViewTreeObserver().removeOnDrawListener(mViewCapture);
+            }
+            mViewCapture = new ViewCapture(root);
+            root.getViewTreeObserver().addOnDrawListener(mViewCapture);
+        }
     }
 
     @Override
@@ -2997,6 +3016,10 @@
         writer.println(prefix + "\tmRotationHelper: " + mRotationHelper);
         writer.println(prefix + "\tmAppWidgetHost.isListening: " + mAppWidgetHost.isListening());
 
+        if (mViewCapture != null) {
+            writer.println(prefix + "\tmViewCapture: " + mViewCapture.dumpToString());
+        }
+
         // Extra logging for general debugging
         mDragLayer.dump(prefix, writer);
         mStateManager.dump(prefix, writer);
@@ -3225,7 +3248,12 @@
      * @param progress Transition progress from 0 to 1; where 0 => home and 1 => widgets.
      */
     public void onWidgetsTransition(float progress) {
-        // No-Op
+        if (mDeviceProfile.isTablet) {
+            float scale =
+                    Utilities.comp(Utilities.comp(mDeviceProfile.workspaceContentScale) * progress);
+            WORKSPACE_WIDGET_SCALE.set(getWorkspace(), scale);
+            HOTSEAT_WIDGET_SCALE.set(getHotseat(), scale);
+        }
     }
 
     private static class NonConfigInstance {
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index 0c7c311..b858d1a 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -81,9 +81,9 @@
             new MultiScalePropertyFactory<Hotseat>("hotseat_scale_property");
 
     public static final int SCALE_INDEX_UNFOLD_ANIMATION = 1;
-    public static final int SCALE_INDEX_UNLOCK_ANIMATION = 2;
-    public static final int SCALE_INDEX_WORKSPACE_STATE = 3;
-    public static final int SCALE_INDEX_REVEAL_ANIM = 4;
+    public static final int SCALE_INDEX_WORKSPACE_STATE = 2;
+    public static final int SCALE_INDEX_REVEAL_ANIM = 3;
+    public static final int SCALE_INDEX_WIDGET_TRANSITION = 4;
 
     /** Increase the duration if we prevented the fling, as we are going against a high velocity. */
     public static int blockedFlingDurationFactor(float velocity) {
diff --git a/src/com/android/launcher3/allapps/WorkEduCard.java b/src/com/android/launcher3/allapps/WorkEduCard.java
index 4feeabb..539cff1 100644
--- a/src/com/android/launcher3/allapps/WorkEduCard.java
+++ b/src/com/android/launcher3/allapps/WorkEduCard.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.getTabWidth;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
@@ -85,14 +87,10 @@
     }
 
     @Override
-    public void onAnimationRepeat(Animation animation) {
-
-    }
+    public void onAnimationRepeat(Animation animation) { }
 
     @Override
-    public void onAnimationStart(Animation animation) {
-
-    }
+    public void onAnimationStart(Animation animation) { }
 
     private void removeCard() {
         if (mPosition == -1) {
@@ -105,8 +103,14 @@
         }
     }
 
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int size = MeasureSpec.getSize(widthMeasureSpec);
+        findViewById(R.id.wrapper).getLayoutParams().width = getTabWidth(getContext(), size);
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
     public void setPosition(int position) {
         mPosition = position;
     }
-
 }
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index bd24b77..aee7c4c 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.allapps;
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TURN_OFF_WORK_APPS_TAP;
+import static com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.getTabWidth;
 
 import android.content.Context;
 import android.graphics.Insets;
@@ -96,7 +97,6 @@
             }
 
             DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
-            lp.rightMargin = lp.leftMargin = dp.allAppsLeftRightPadding;
             if (!dp.isGestureMode) {
                 if (dp.isTaskbarPresent) {
                     bottomMargin += dp.taskbarSize;
@@ -110,6 +110,18 @@
     }
 
     @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
+        View parent = (View) getParent();
+        int size = parent.getWidth() - parent.getPaddingLeft() - parent.getPaddingRight()
+                - 2 * dp.allAppsLeftRightPadding;
+        int tabWidth = getTabWidth(getContext(), size);
+        int shift = (size - tabWidth) / 2 + dp.allAppsLeftRightPadding;
+        setTranslationX(Utilities.isRtl(getResources()) ? shift : -shift);
+    }
+
+    @Override
     public void onActivePageChanged(int page) {
         mOnWorkTab = page == ActivityAllAppsContainerView.AdapterHolder.WORK;
         updateVisibility();
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index b7f3dad..4fd13b2 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -285,6 +285,9 @@
             "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", false,
             "Use local overrides for search request timeout");
 
+    public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag(
+            "CONTINUOUS_VIEW_TREE_CAPTURE", false, "Capture View tree every frame");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 7f444d6..0334b96 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -206,7 +206,7 @@
                             /* spanX= */ 1, /* spanY= */ 1);
                     // TODO(b/234322284): return the real center point.
                     return new Point(cellRect.left + (cellRect.right - cellRect.left) / 3,
-                            cellRect.centerY());
+                            cellRect.top + (cellRect.bottom - cellRect.top) / 3);
                 });
             }
 
diff --git a/src/com/android/launcher3/util/ViewCapture.java b/src/com/android/launcher3/util/ViewCapture.java
new file mode 100644
index 0000000..140971b
--- /dev/null
+++ b/src/com/android/launcher3/util/ViewCapture.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.util.Base64;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnDrawListener;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.view.ViewCaptureData.ExportedData;
+import com.android.launcher3.view.ViewCaptureData.FrameData;
+import com.android.launcher3.view.ViewCaptureData.ViewNode;
+
+import java.util.concurrent.FutureTask;
+
+/**
+ * Utility class for capturing view data every frame
+ */
+public class ViewCapture implements OnDrawListener {
+
+    private static final String TAG = "ViewCapture";
+
+    private static final int MEMORY_SIZE = 2000;
+
+    private final View mRoot;
+    private final long[] mFrameTimes = new long[MEMORY_SIZE];
+    private final Node[] mNodes = new Node[MEMORY_SIZE];
+
+    private int mFrameIndex = -1;
+
+    /**
+     * @param root the root view for the capture data
+     */
+    public ViewCapture(View root) {
+        mRoot = root;
+    }
+
+    @Override
+    public void onDraw() {
+        Trace.beginSection("view_capture");
+        long now = SystemClock.elapsedRealtimeNanos();
+
+        mFrameIndex++;
+        if (mFrameIndex >= MEMORY_SIZE) {
+            mFrameIndex = 0;
+        }
+        mFrameTimes[mFrameIndex] = now;
+        mNodes[mFrameIndex] = captureView(mRoot, mNodes[mFrameIndex]);
+        Trace.endSection();
+    }
+
+    /**
+     * Creates a proto of all the data captured so far.
+     */
+    public String dumpToString() {
+        Handler handler = mRoot.getHandler();
+        if (handler == null) {
+            handler = Executors.MAIN_EXECUTOR.getHandler();
+        }
+        FutureTask<ExportedData> task = new FutureTask<>(this::dumpToProtoUI);
+        if (Looper.myLooper() == handler.getLooper()) {
+            task.run();
+        } else {
+            handler.post(task);
+        }
+        try {
+            return Base64.encodeToString(task.get().toByteArray(),
+                    Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
+        } catch (Exception e) {
+            Log.e(TAG, "Error capturing proto", e);
+            return "--error--";
+        }
+    }
+
+    @UiThread
+    private ExportedData dumpToProtoUI() {
+        ExportedData.Builder dataBuilder = ExportedData.newBuilder();
+        Resources res = mRoot.getResources();
+
+        int size = (mNodes[MEMORY_SIZE - 1] == null) ? mFrameIndex + 1 : MEMORY_SIZE;
+        for (int i = size - 1; i >= 0; i--) {
+            int index = (MEMORY_SIZE + mFrameIndex - i) % MEMORY_SIZE;
+            dataBuilder.addFrameData(FrameData.newBuilder()
+                    .setNode(mNodes[index].toProto(res))
+                    .setTimestamp(mFrameTimes[index]));
+        }
+        return dataBuilder.build();
+    }
+
+    private Node captureView(View view, Node recycle) {
+        Node result = recycle == null ? new Node() : recycle;
+
+        result.clazz = view.getClass();
+        result.hashCode = view.hashCode();
+        result.id = view.getId();
+        result.left = view.getLeft();
+        result.top = view.getTop();
+        result.right = view.getRight();
+        result.bottom = view.getBottom();
+        result.scrollX = view.getScrollX();
+        result.scrollY = view.getScrollY();
+
+        result.translateX = view.getTranslationX();
+        result.translateY = view.getTranslationY();
+        result.scaleX = view.getScaleX();
+        result.scaleY = view.getScaleY();
+        result.alpha = view.getAlpha();
+
+        result.visibility = view.getVisibility();
+        result.willNotDraw = view.willNotDraw();
+
+        if (view instanceof ViewGroup) {
+            ViewGroup parent = (ViewGroup) view;
+            result.clipChildren = parent.getClipChildren();
+            int childCount = parent.getChildCount();
+            if (childCount == 0) {
+                result.children = null;
+            } else {
+                result.children = captureView(parent.getChildAt(0), result.children);
+                Node lastChild = result.children;
+                for (int i = 1; i < childCount; i++) {
+                    lastChild.sibling = captureView(parent.getChildAt(i), lastChild.sibling);
+                    lastChild = lastChild.sibling;
+                }
+                lastChild.sibling = null;
+            }
+        } else {
+            result.clipChildren = false;
+            result.children = null;
+        }
+        return result;
+    }
+
+    private static class Node {
+
+        // We store reference in memory to avoid generating and storing too many strings
+        public Class clazz;
+        public int hashCode;
+
+        public int id;
+        public int left, top, right, bottom;
+        public int scrollX, scrollY;
+
+        public float translateX, translateY;
+        public float scaleX, scaleY;
+        public float alpha;
+
+        public int visibility;
+        public boolean willNotDraw;
+        public boolean clipChildren;
+
+        public Node sibling;
+        public Node children;
+
+        public ViewNode toProto(Resources res) {
+            String resolvedId;
+            if (id >= 0) {
+                try {
+                    resolvedId = res.getResourceTypeName(id) + '/' + res.getResourceEntryName(id);
+                } catch (Resources.NotFoundException e) {
+                    resolvedId = "id/" + "0x" + Integer.toHexString(id).toUpperCase();
+                }
+            } else {
+                resolvedId = "NO_ID";
+            }
+
+            ViewNode.Builder result = ViewNode.newBuilder()
+                    .setClassname(clazz.getName() + "@" + hashCode)
+                    .setId(resolvedId)
+                    .setLeft(left)
+                    .setTop(top)
+                    .setWidth(right - left)
+                    .setHeight(bottom - top)
+                    .setTranslationX(translateX)
+                    .setTranslationY(translateY)
+                    .setScaleX(scaleX)
+                    .setScaleY(scaleY)
+                    .setAlpha(alpha)
+                    .setVisibility(visibility)
+                    .setWillNotDraw(willNotDraw)
+                    .setClipChildren(clipChildren);
+            Node child = children;
+            while (child != null) {
+                result.addChildren(child.toProto(res));
+                child = child.sibling;
+            }
+            return result.build();
+        }
+
+    }
+}
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index f553fb4..800b1f6 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -22,13 +22,11 @@
 
 import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
 
-import android.annotation.TargetApi;
 import android.app.WallpaperManager;
 import android.content.Context;
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.os.Build;
 import android.util.AttributeSet;
 import android.util.Property;
 import android.view.MotionEvent;
@@ -550,18 +548,24 @@
     }
 
     @Override
-    @TargetApi(Build.VERSION_CODES.Q)
     public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
         if (Utilities.ATLEAST_Q) {
             Insets gestureInsets = insets.getMandatorySystemGestureInsets();
             int gestureInsetBottom = gestureInsets.bottom;
+            Insets imeInset = Utilities.ATLEAST_R
+                    ? insets.getInsets(WindowInsets.Type.ime())
+                    : Insets.NONE;
             DeviceProfile dp = mActivity.getDeviceProfile();
             if (dp.isTaskbarPresent) {
                 // Ignore taskbar gesture insets to avoid interfering with TouchControllers.
                 gestureInsetBottom = Math.max(0, gestureInsetBottom - dp.taskbarSize);
             }
-            mSystemGestureRegion.set(gestureInsets.left, gestureInsets.top,
-                    gestureInsets.right, gestureInsetBottom);
+            mSystemGestureRegion.set(
+                    Math.max(gestureInsets.left, imeInset.left),
+                    Math.max(gestureInsets.top, imeInset.top),
+                    Math.max(gestureInsets.right, imeInset.right),
+                    Math.max(gestureInsetBottom, imeInset.bottom)
+            );
         }
         return super.dispatchApplyWindowInsets(insets);
     }
diff --git a/src/com/android/launcher3/views/StickyHeaderLayout.java b/src/com/android/launcher3/views/StickyHeaderLayout.java
new file mode 100644
index 0000000..d6481a9
--- /dev/null
+++ b/src/com/android/launcher3/views/StickyHeaderLayout.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2021 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.views;
+
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.launcher3.R;
+
+/**
+ * A {@link LinearLayout} container which allows scrolling parts of its content based on the
+ * scroll of a different view. Views which are marked as sticky are not scrolled, giving the
+ * illusion of a sticky header.
+ */
+public class StickyHeaderLayout extends LinearLayout implements
+        RecyclerView.OnChildAttachStateChangeListener {
+
+    private static final FloatProperty<StickyHeaderLayout> SCROLL_OFFSET =
+            new FloatProperty<StickyHeaderLayout>("scrollAnimOffset") {
+                @Override
+                public void setValue(StickyHeaderLayout view, float offset) {
+                    view.mScrollOffset = offset;
+                    view.updateHeaderScroll();
+                }
+
+                @Override
+                public Float get(StickyHeaderLayout view) {
+                    return view.mScrollOffset;
+                }
+            };
+
+    private static final MotionEventProxyMethod INTERCEPT_PROXY = ViewGroup::onInterceptTouchEvent;
+    private static final MotionEventProxyMethod TOUCH_PROXY = ViewGroup::onTouchEvent;
+
+    private RecyclerView mCurrentRecyclerView;
+    private EmptySpaceView mCurrentEmptySpaceView;
+
+    private float mLastScroll = 0;
+    private float mScrollOffset = 0;
+    private Animator mOffsetAnimator;
+
+    private boolean mShouldForwardToRecyclerView = false;
+    private int mHeaderHeight;
+
+    public StickyHeaderLayout(Context context) {
+        this(context, /* attrs= */ null);
+    }
+
+    public StickyHeaderLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, /* defStyleAttr= */ 0);
+    }
+
+    public StickyHeaderLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
+    }
+
+    public StickyHeaderLayout(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /**
+     * Sets the recycler view, this sticky header should track
+     */
+    public void setCurrentRecyclerView(RecyclerView currentRecyclerView) {
+        boolean animateReset = mCurrentRecyclerView != null;
+        if (mCurrentRecyclerView != null) {
+            mCurrentRecyclerView.removeOnChildAttachStateChangeListener(this);
+        }
+        mCurrentRecyclerView = currentRecyclerView;
+        mCurrentRecyclerView.addOnChildAttachStateChangeListener(this);
+        findCurrentEmptyView();
+        reset(animateReset);
+    }
+
+    public int getHeaderHeight() {
+        return mHeaderHeight;
+    }
+
+    private void updateHeaderScroll() {
+        mLastScroll = getCurrentScroll();
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams();
+            child.setTranslationY(Math.max(mLastScroll, lp.scrollLimit));
+        }
+    }
+
+    private float getCurrentScroll() {
+        return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        mHeaderHeight = getMeasuredHeight();
+        if (mCurrentEmptySpaceView != null) {
+            mCurrentEmptySpaceView.setFixedHeight(mHeaderHeight);
+        }
+    }
+
+    /** Resets any previous view translation. */
+    public void reset(boolean animate) {
+        if (mOffsetAnimator != null) {
+            mOffsetAnimator.cancel();
+            mOffsetAnimator = null;
+        }
+
+        mScrollOffset = 0;
+        if (!animate) {
+            updateHeaderScroll();
+        } else {
+            float startValue = mLastScroll - getCurrentScroll();
+            mOffsetAnimator = ObjectAnimator.ofFloat(this, SCROLL_OFFSET, startValue, 0);
+            mOffsetAnimator.addListener(forEndCallback(() -> mOffsetAnimator = null));
+            mOffsetAnimator.start();
+        }
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        return (mShouldForwardToRecyclerView = proxyMotionEvent(event, INTERCEPT_PROXY))
+                || super.onInterceptTouchEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return mShouldForwardToRecyclerView && proxyMotionEvent(event, TOUCH_PROXY)
+                || super.onTouchEvent(event);
+    }
+
+    private boolean proxyMotionEvent(MotionEvent event, MotionEventProxyMethod method) {
+        float dx = mCurrentRecyclerView.getLeft() - getLeft();
+        float dy = mCurrentRecyclerView.getTop() - getTop();
+        event.offsetLocation(dx, dy);
+        try {
+            return method.proxyEvent(mCurrentRecyclerView, event);
+        } finally {
+            event.offsetLocation(-dx, -dy);
+        }
+    }
+
+    @Override
+    public void onChildViewAttachedToWindow(@NonNull View view) {
+        if (view instanceof EmptySpaceView) {
+            findCurrentEmptyView();
+        }
+    }
+
+    @Override
+    public void onChildViewDetachedFromWindow(@NonNull View view) {
+        if (view == mCurrentEmptySpaceView) {
+            findCurrentEmptyView();
+        }
+    }
+
+    private void findCurrentEmptyView() {
+        if (mCurrentEmptySpaceView != null) {
+            mCurrentEmptySpaceView.setOnYChangeCallback(null);
+            mCurrentEmptySpaceView = null;
+        }
+        int childCount = mCurrentRecyclerView.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View view = mCurrentRecyclerView.getChildAt(i);
+            if (view instanceof EmptySpaceView) {
+                mCurrentEmptySpaceView = (EmptySpaceView) view;
+                mCurrentEmptySpaceView.setFixedHeight(getHeaderHeight());
+                mCurrentEmptySpaceView.setOnYChangeCallback(this::updateHeaderScroll);
+                return;
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        // Update various stick parameters
+        int count = getChildCount();
+        int stickyHeaderHeight = 0;
+        for (int i = 0; i < count; i++) {
+            View v = getChildAt(i);
+            MyLayoutParams lp = (MyLayoutParams) v.getLayoutParams();
+            if (lp.sticky) {
+                lp.scrollLimit = -v.getTop() + stickyHeaderHeight;
+                stickyHeaderHeight += v.getHeight();
+            } else {
+                lp.scrollLimit = Integer.MIN_VALUE;
+            }
+        }
+        updateHeaderScroll();
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new MyLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        return new MyLayoutParams(lp.width, lp.height);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new MyLayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof MyLayoutParams;
+    }
+
+    private static class MyLayoutParams extends LayoutParams {
+
+        public final boolean sticky;
+        public int scrollLimit;
+
+        MyLayoutParams(int width, int height) {
+            super(width, height);
+            sticky = false;
+        }
+
+        MyLayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.StickyScroller_Layout);
+            sticky = a.getBoolean(R.styleable.StickyScroller_Layout_layout_sticky, false);
+            a.recycle();
+        }
+    }
+
+    private interface MotionEventProxyMethod {
+
+        boolean proxyEvent(ViewGroup view, MotionEvent event);
+    }
+
+    /**
+     * Empty view which allows listening for 'Y' changes
+     */
+    public static class EmptySpaceView extends View {
+
+        private Runnable mOnYChangeCallback;
+        private int mHeight = 0;
+
+        public EmptySpaceView(Context context) {
+            super(context);
+            animate().setUpdateListener(v -> notifyYChanged());
+        }
+
+        /**
+         * Sets the height for the empty view
+         * @return true if the height changed, false otherwise
+         */
+        public boolean setFixedHeight(int height) {
+            if (mHeight != height) {
+                mHeight = height;
+                requestLayout();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            super.onMeasure(widthMeasureSpec, makeMeasureSpec(mHeight, EXACTLY));
+        }
+
+        public void setOnYChangeCallback(Runnable callback) {
+            mOnYChangeCallback = callback;
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            super.onLayout(changed, left, top, right, bottom);
+            notifyYChanged();
+        }
+
+        @Override
+        public void offsetTopAndBottom(int offset) {
+            super.offsetTopAndBottom(offset);
+            notifyYChanged();
+        }
+
+        @Override
+        public void setTranslationY(float translationY) {
+            super.setTranslationY(translationY);
+            notifyYChanged();
+        }
+
+        private void notifyYChanged() {
+            if (mOnYChangeCallback != null) {
+                mOnYChangeCallback.run();
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
deleted file mode 100644
index 716dcf3..0000000
--- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget.picker;
-
-import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
-
-import android.animation.Animator;
-import android.animation.ObjectAnimator;
-import android.util.FloatProperty;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.android.launcher3.R;
-import com.android.launcher3.widget.picker.WidgetsSpaceViewHolderBinder.EmptySpaceView;
-import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
-
-/**
- * A controller which measures & updates {@link WidgetsFullSheet}'s views padding, margin and
- * vertical displacement upon scrolling.
- */
-final class SearchAndRecommendationsScrollController implements
-        RecyclerView.OnChildAttachStateChangeListener {
-
-    private static final FloatProperty<SearchAndRecommendationsScrollController> SCROLL_OFFSET =
-            new FloatProperty<SearchAndRecommendationsScrollController>("scrollAnimOffset") {
-        @Override
-        public void setValue(SearchAndRecommendationsScrollController controller, float offset) {
-            controller.mScrollOffset = offset;
-            controller.updateHeaderScroll();
-        }
-
-        @Override
-        public Float get(SearchAndRecommendationsScrollController controller) {
-            return controller.mScrollOffset;
-        }
-    };
-
-    private static final MotionEventProxyMethod INTERCEPT_PROXY = ViewGroup::onInterceptTouchEvent;
-    private static final MotionEventProxyMethod TOUCH_PROXY = ViewGroup::onTouchEvent;
-
-    final SearchAndRecommendationsView mContainer;
-    final View mSearchBarContainer;
-    final WidgetsSearchBar mSearchBar;
-    final TextView mHeaderTitle;
-    final WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
-    @Nullable final View mTabBar;
-
-    private WidgetsRecyclerView mCurrentRecyclerView;
-    private EmptySpaceView mCurrentEmptySpaceView;
-
-    private float mLastScroll = 0;
-    private float mScrollOffset = 0;
-    private Animator mOffsetAnimator;
-
-    private boolean mShouldForwardToRecyclerView = false;
-
-    private int mHeaderHeight;
-
-    SearchAndRecommendationsScrollController(
-            SearchAndRecommendationsView searchAndRecommendationContainer) {
-        mContainer = searchAndRecommendationContainer;
-        mSearchBarContainer = mContainer.findViewById(R.id.search_bar_container);
-        mSearchBar = mContainer.findViewById(R.id.widgets_search_bar);
-        mHeaderTitle = mContainer.findViewById(R.id.title);
-        mRecommendedWidgetsTable = mContainer.findViewById(R.id.recommended_widget_table);
-        mTabBar = mContainer.findViewById(R.id.tabs);
-
-        mContainer.setSearchAndRecommendationScrollController(this);
-    }
-
-    public void setCurrentRecyclerView(WidgetsRecyclerView currentRecyclerView) {
-        boolean animateReset = mCurrentRecyclerView != null;
-        if (mCurrentRecyclerView != null) {
-            mCurrentRecyclerView.removeOnChildAttachStateChangeListener(this);
-        }
-        mCurrentRecyclerView = currentRecyclerView;
-        mCurrentRecyclerView.addOnChildAttachStateChangeListener(this);
-        findCurrentEmptyView();
-        reset(animateReset);
-    }
-
-    public int getHeaderHeight() {
-        return mHeaderHeight;
-    }
-
-    private void updateHeaderScroll() {
-        mLastScroll = getCurrentScroll();
-        mHeaderTitle.setTranslationY(mLastScroll);
-        mRecommendedWidgetsTable.setTranslationY(mLastScroll);
-
-        float searchYDisplacement = Math.max(mLastScroll, -mSearchBarContainer.getTop());
-        mSearchBarContainer.setTranslationY(searchYDisplacement);
-
-        if (mTabBar != null) {
-            float tabsDisplacement = Math.max(mLastScroll, -mTabBar.getTop()
-                    + mSearchBarContainer.getHeight());
-            mTabBar.setTranslationY(tabsDisplacement);
-        }
-    }
-
-    private float getCurrentScroll() {
-        return mScrollOffset + (mCurrentEmptySpaceView == null ? 0 : mCurrentEmptySpaceView.getY());
-    }
-
-    /**
-     * Updates the scrollable header height
-     *
-     * @return {@code true} if the header height or dependent property changed.
-     */
-    public boolean updateHeaderHeight() {
-        boolean hasSizeUpdated = false;
-
-        int headerHeight = mContainer.getMeasuredHeight();
-        if (headerHeight != mHeaderHeight) {
-            mHeaderHeight = headerHeight;
-            hasSizeUpdated = true;
-        }
-
-        if (mCurrentEmptySpaceView != null
-                && mCurrentEmptySpaceView.setFixedHeight(mHeaderHeight)) {
-            hasSizeUpdated = true;
-        }
-        return hasSizeUpdated;
-    }
-
-    /** Resets any previous view translation. */
-    public void reset(boolean animate) {
-        if (mOffsetAnimator != null) {
-            mOffsetAnimator.cancel();
-            mOffsetAnimator = null;
-        }
-
-        mScrollOffset = 0;
-        if (!animate) {
-            updateHeaderScroll();
-        } else {
-            float startValue = mLastScroll - getCurrentScroll();
-            mOffsetAnimator = ObjectAnimator.ofFloat(this, SCROLL_OFFSET, startValue, 0);
-            mOffsetAnimator.addListener(forEndCallback(() -> mOffsetAnimator = null));
-            mOffsetAnimator.start();
-        }
-    }
-
-    /**
-     * Returns {@code true} if a touch event should be intercepted by this controller.
-     */
-    public boolean onInterceptTouchEvent(MotionEvent event) {
-        return (mShouldForwardToRecyclerView = proxyMotionEvent(event, INTERCEPT_PROXY));
-    }
-
-    /**
-     * Returns {@code true} if this controller has intercepted and consumed a touch event.
-     */
-    public boolean onTouchEvent(MotionEvent event) {
-        return mShouldForwardToRecyclerView && proxyMotionEvent(event, TOUCH_PROXY);
-    }
-
-    private boolean proxyMotionEvent(MotionEvent event, MotionEventProxyMethod method) {
-        float dx = mCurrentRecyclerView.getLeft() - mContainer.getLeft();
-        float dy = mCurrentRecyclerView.getTop() - mContainer.getTop();
-        event.offsetLocation(dx, dy);
-        try {
-            return method.proxyEvent(mCurrentRecyclerView, event);
-        } finally {
-            event.offsetLocation(-dx, -dy);
-        }
-    }
-
-    @Override
-    public void onChildViewAttachedToWindow(@NonNull View view) {
-        if (view instanceof EmptySpaceView) {
-            findCurrentEmptyView();
-        }
-    }
-
-    @Override
-    public void onChildViewDetachedFromWindow(@NonNull View view) {
-        if (view == mCurrentEmptySpaceView) {
-            findCurrentEmptyView();
-        }
-    }
-
-    private void findCurrentEmptyView() {
-        if (mCurrentEmptySpaceView != null) {
-            mCurrentEmptySpaceView.setOnYChangeCallback(null);
-            mCurrentEmptySpaceView = null;
-        }
-        int childCount = mCurrentRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View view = mCurrentRecyclerView.getChildAt(i);
-            if (view instanceof EmptySpaceView) {
-                mCurrentEmptySpaceView = (EmptySpaceView) view;
-                mCurrentEmptySpaceView.setFixedHeight(getHeaderHeight());
-                mCurrentEmptySpaceView.setOnYChangeCallback(this::updateHeaderScroll);
-                return;
-            }
-        }
-    }
-
-    private interface MotionEventProxyMethod {
-
-        boolean proxyEvent(ViewGroup view, MotionEvent event);
-    }
-}
diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsView.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsView.java
deleted file mode 100644
index 0d7d2b5..0000000
--- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsView.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.widget.picker;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.widget.LinearLayout;
-
-/**
- * A {@link LinearLayout} container for holding search and widgets recommendation.
- *
- * <p>This class intercepts touch events and dispatch them to the right view.
- */
-public class SearchAndRecommendationsView extends LinearLayout {
-    private SearchAndRecommendationsScrollController mController;
-
-    public SearchAndRecommendationsView(Context context) {
-        this(context, /* attrs= */ null);
-    }
-
-    public SearchAndRecommendationsView(Context context, AttributeSet attrs) {
-        this(context, attrs, /* defStyleAttr= */ 0);
-    }
-
-    public SearchAndRecommendationsView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
-    }
-
-    public SearchAndRecommendationsView(Context context, AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
-    public void setSearchAndRecommendationScrollController(
-            SearchAndRecommendationsScrollController controller) {
-        mController = controller;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent event) {
-        return mController.onInterceptTouchEvent(event) || super.onInterceptTouchEvent(event);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        return mController.onTouchEvent(event) || super.onTouchEvent(event);
-    }
-}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index a49cdc0..88d9723 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -62,11 +62,13 @@
 import com.android.launcher3.views.ArrowTipView;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.SpringRelativeLayout;
+import com.android.launcher3.views.StickyHeaderLayout;
 import com.android.launcher3.views.WidgetsEduView;
 import com.android.launcher3.widget.BaseWidgetSheet;
 import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 import com.android.launcher3.widget.picker.search.SearchModeListener;
+import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
 import com.android.launcher3.widget.util.WidgetsTableUtils;
 import com.android.launcher3.workprofile.PersonalWorkPagedView;
 import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
@@ -161,7 +163,13 @@
     private boolean mIsNoWidgetsViewNeeded;
     private int mMaxSpansPerRow = DEFAULT_MAX_HORIZONTAL_SPANS;
     private TextView mNoWidgetsView;
-    private SearchAndRecommendationsScrollController mSearchScrollController;
+
+    private StickyHeaderLayout mSearchScrollView;
+    private WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
+    private View mTabBar;
+    private View mSearchBarContainer;
+    private WidgetsSearchBar mSearchBar;
+    private TextView mHeaderTitle;
 
     public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
@@ -214,17 +222,23 @@
         }
 
         mNoWidgetsView = findViewById(R.id.no_widgets_text);
-        mSearchScrollController = new SearchAndRecommendationsScrollController(
-                findViewById(R.id.search_and_recommendations_container));
-        mSearchScrollController.setCurrentRecyclerView(
-                findViewById(R.id.primary_widgets_list_view));
-        mSearchScrollController.mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
-        mSearchScrollController.mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
+
+        mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
+        mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
+
+        mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
+        mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
+        mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
+
+        mTabBar = mSearchScrollView.findViewById(R.id.tabs);
+        mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
+        mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
+        mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
 
         onRecommendedWidgetsBound();
         onWidgetsBound();
 
-        mSearchScrollController.mSearchBar.initialize(
+        mSearchBar.initialize(
                 mActivityContext.getPopupDataProvider(), /* searchModeListener= */ this);
 
         setUpEducationViewsIfNeeded();
@@ -258,7 +272,7 @@
             reset();
             resetExpandedHeaders();
             mCurrentWidgetsRecyclerView = recyclerView;
-            mSearchScrollController.setCurrentRecyclerView(recyclerView);
+            mSearchScrollView.setCurrentRecyclerView(recyclerView);
         }
     }
 
@@ -285,7 +299,7 @@
             mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop();
         }
         mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
-        mSearchScrollController.reset(/* animate= */ true);
+        mSearchScrollView.reset(/* animate= */ true);
     }
 
     @VisibleForTesting
@@ -355,8 +369,7 @@
 
     @Override
     protected void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx) {
-        setContentViewChildHorizontalMargin(mSearchScrollController.mContainer,
-                contentHorizontalMarginInPx);
+        setContentViewChildHorizontalMargin(mSearchScrollView, contentHorizontalMarginInPx);
         if (mViewPager == null) {
             setContentViewChildHorizontalPadding(
                     mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView,
@@ -390,16 +403,8 @@
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         doMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        if (mSearchScrollController.updateHeaderHeight()) {
-            doMeasure(widthMeasureSpec, heightMeasureSpec);
-        }
-
         if (updateMaxSpansPerRow()) {
             doMeasure(widthMeasureSpec, heightMeasureSpec);
-
-            if (mSearchScrollController.updateHeaderHeight()) {
-                doMeasure(widthMeasureSpec, heightMeasureSpec);
-            }
         }
     }
 
@@ -460,7 +465,7 @@
 
         if (mHasWorkProfile) {
             mViewPager.setVisibility(VISIBLE);
-            mSearchScrollController.mTabBar.setVisibility(VISIBLE);
+            mTabBar.setVisibility(VISIBLE);
             AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
             workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
             onActivePageChanged(mViewPager.getCurrentPage());
@@ -508,10 +513,10 @@
     private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) {
         mIsInSearchMode = isInSearchMode;
         if (isInSearchMode) {
-            mSearchScrollController.mRecommendedWidgetsTable.setVisibility(GONE);
+            mRecommendedWidgetsTable.setVisibility(GONE);
             if (mHasWorkProfile) {
                 mViewPager.setVisibility(GONE);
-                mSearchScrollController.mTabBar.setVisibility(GONE);
+                mTabBar.setVisibility(GONE);
             } else {
                 mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView.setVisibility(GONE);
             }
@@ -539,7 +544,6 @@
         }
         List<WidgetItem> recommendedWidgets =
                 mActivityContext.getPopupDataProvider().getRecommendedWidgets();
-        WidgetsRecommendationTableLayout table = mSearchScrollController.mRecommendedWidgetsTable;
         if (recommendedWidgets.size() > 0) {
             float noWidgetsViewHeight = 0;
             if (mIsNoWidgetsViewNeeded) {
@@ -562,9 +566,10 @@
             List<ArrayList<WidgetItem>> recommendedWidgetsInTable =
                     WidgetsTableUtils.groupWidgetItemsIntoTableWithoutReordering(
                             recommendedWidgets, mMaxSpansPerRow);
-            table.setRecommendedWidgets(recommendedWidgetsInTable, maxTableHeight);
+            mRecommendedWidgetsTable.setRecommendedWidgets(
+                    recommendedWidgetsInTable, maxTableHeight);
         } else {
-            table.setVisibility(GONE);
+            mRecommendedWidgetsTable.setVisibility(GONE);
         }
     }
 
@@ -619,10 +624,9 @@
                 mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer());
             }
 
-            if (mSearchScrollController.mSearchBar.isSearchBarFocused()
-                    && !getPopupContainer().isEventOverView(
-                    mSearchScrollController.mSearchBarContainer, ev)) {
-                mSearchScrollController.mSearchBar.clearSearchBarFocus();
+            if (mSearchBar.isSearchBarFocused()
+                    && !getPopupContainer().isEventOverView(mSearchBarContainer, ev)) {
+                mSearchBar.clearSearchBarFocus();
             }
         }
         return super.onControllerInterceptTouchEvent(ev);
@@ -663,8 +667,8 @@
 
     @Override
     public int getHeaderViewHeight() {
-        return measureHeightWithVerticalMargins(mSearchScrollController.mHeaderTitle)
-                + measureHeightWithVerticalMargins(mSearchScrollController.mSearchBarContainer);
+        return measureHeightWithVerticalMargins(mHeaderTitle)
+                + measureHeightWithVerticalMargins(mSearchBarContainer);
     }
 
     /** private the height, in pixel, + the vertical margins of a given view. */
@@ -681,14 +685,14 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         if (mIsInSearchMode) {
-            mSearchScrollController.mSearchBar.reset();
+            mSearchBar.reset();
         }
     }
 
     @Override
     public boolean onBackPressed() {
         if (mIsInSearchMode) {
-            mSearchScrollController.mSearchBar.reset();
+            mSearchBar.reset();
             return true;
         }
         return super.onBackPressed();
@@ -701,10 +705,9 @@
     }
 
     @Nullable private View getViewToShowEducationTip() {
-        if (mSearchScrollController.mRecommendedWidgetsTable.getVisibility() == VISIBLE
-                && mSearchScrollController.mRecommendedWidgetsTable.getChildCount() > 0) {
-            return ((ViewGroup) mSearchScrollController.mRecommendedWidgetsTable.getChildAt(0))
-                    .getChildAt(0);
+        if (mRecommendedWidgetsTable.getVisibility() == VISIBLE
+                && mRecommendedWidgetsTable.getChildCount() > 0) {
+            return ((ViewGroup) mRecommendedWidgetsTable.getChildAt(0)).getChildAt(0);
         }
 
         AdapterHolder adapterHolder = mAdapters.get(mIsInSearchMode
@@ -801,7 +804,7 @@
         }
 
         private int getEmptySpaceHeight() {
-            return mSearchScrollController.getHeaderHeight();
+            return mSearchScrollView.getHeaderHeight();
         }
 
         void setup(WidgetsRecyclerView recyclerView) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsSpaceViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsSpaceViewHolderBinder.java
index 1aa5753..0c4f7aa 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsSpaceViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsSpaceViewHolderBinder.java
@@ -15,16 +15,12 @@
  */
 package com.android.launcher3.widget.picker;
 
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
-import android.content.Context;
-import android.view.View;
 import android.view.ViewGroup;
 
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import com.android.launcher3.recyclerview.ViewHolderBinder;
+import com.android.launcher3.views.StickyHeaderLayout.EmptySpaceView;
 import com.android.launcher3.widget.model.WidgetListSpaceEntry;
 
 import java.util.List;
@@ -52,64 +48,4 @@
             @ListPosition int position, List<Object> payloads) {
         ((EmptySpaceView) holder.itemView).setFixedHeight(mEmptySpaceHeightProvider.getAsInt());
     }
-
-    /**
-     * Empty view which allows listening for 'Y' changes
-     */
-    public static class EmptySpaceView extends View {
-
-        private Runnable mOnYChangeCallback;
-        private int mHeight = 0;
-
-        private EmptySpaceView(Context context) {
-            super(context);
-            animate().setUpdateListener(v -> notifyYChanged());
-        }
-
-        /**
-         * Sets the height for the empty view
-         * @return true if the height changed, false otherwise
-         */
-        public boolean setFixedHeight(int height) {
-            if (mHeight != height) {
-                mHeight = height;
-                requestLayout();
-                return true;
-            }
-            return false;
-        }
-
-        @Override
-        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-            super.onMeasure(widthMeasureSpec, makeMeasureSpec(mHeight, EXACTLY));
-        }
-
-        public void setOnYChangeCallback(Runnable callback) {
-            mOnYChangeCallback = callback;
-        }
-
-        @Override
-        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-            super.onLayout(changed, left, top, right, bottom);
-            notifyYChanged();
-        }
-
-        @Override
-        public void offsetTopAndBottom(int offset) {
-            super.offsetTopAndBottom(offset);
-            notifyYChanged();
-        }
-
-        @Override
-        public void setTranslationY(float translationY) {
-            super.setTranslationY(translationY);
-            notifyYChanged();
-        }
-
-        private void notifyYChanged() {
-            if (mOnYChangeCallback != null) {
-                mOnYChangeCallback.run();
-            }
-        }
-    }
 }
diff --git a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
index 11185fb..49db2a0 100644
--- a/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
+++ b/src/com/android/launcher3/workprofile/PersonalWorkSlidingTabStrip.java
@@ -23,7 +23,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * Supports two indicator colors, dedicated for personal and work tabs.
@@ -72,6 +74,26 @@
         return false;
     }
 
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (getPaddingLeft() == 0 && getPaddingRight() == 0) {
+            // If any padding is not specified, restrict the width to emulate padding
+            int size = MeasureSpec.getSize(widthMeasureSpec);
+            size = getTabWidth(getContext(), size);
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    /**
+     * Returns distance between left and right app icons
+     */
+    public static int getTabWidth(Context context, int totalWidth) {
+        DeviceProfile grid = ActivityContext.lookupContext(context).getDeviceProfile();
+        int iconPadding = totalWidth / grid.numShownAllAppsColumns - grid.allAppsIconSizePx;
+        return totalWidth - iconPadding;
+    }
+
     /**
      * Interface definition for a callback to be invoked when an active page has been changed.
      */