Merge "Removing back-button alpha animation code" into tm-qpr-dev
diff --git a/res/layout/all_apps_personal_work_tabs.xml b/res/layout/all_apps_personal_work_tabs.xml
index 11143fb..d15b906 100644
--- a/res/layout/all_apps_personal_work_tabs.xml
+++ b/res/layout/all_apps_personal_work_tabs.xml
@@ -20,6 +20,8 @@
     android:layout_width="match_parent"
     android:layout_height="@dimen/all_apps_header_pill_height"
     android:layout_gravity="center_horizontal"
+    android:paddingTop="@dimen/all_apps_tabs_vertical_padding"
+    android:paddingBottom="@dimen/all_apps_tabs_vertical_padding"
     android:orientation="horizontal"
     style="@style/TextHeadline">
 
@@ -28,7 +30,6 @@
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_marginEnd="@dimen/all_apps_tabs_button_horizontal_padding"
-        android:layout_marginVertical="@dimen/all_apps_tabs_vertical_padding"
         android:layout_weight="1"
         android:background="@drawable/all_apps_tabs_background"
         android:text="@string/all_apps_personal_tab"
@@ -41,7 +42,6 @@
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_marginStart="@dimen/all_apps_tabs_button_horizontal_padding"
-        android:layout_marginVertical="@dimen/all_apps_tabs_vertical_padding"
         android:layout_weight="1"
         android:background="@drawable/all_apps_tabs_background"
         android:text="@string/all_apps_work_tab"
diff --git a/res/layout/search_results_rv_layout.xml b/res/layout/search_results_rv_layout.xml
index 567cb5f..9127521 100644
--- a/res/layout/search_results_rv_layout.xml
+++ b/res/layout/search_results_rv_layout.xml
@@ -18,7 +18,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/search_results_list_view"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     android:clipToPadding="false"
     android:descendantFocusability="afterDescendants"
     android:focusable="true" />
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index d2fa5fa..81b28ba 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -26,6 +26,7 @@
     android:textColor="@color/all_apps_tab_text"
     android:textSize="14sp"
     android:background="@drawable/work_apps_toggle_background"
+    android:forceHasOverlappingRendering="false"
     android:drawablePadding="8dp"
     android:drawableStart="@drawable/ic_corp_off"
     android:layout_marginBottom="@dimen/work_fab_margin_bottom"
diff --git a/src/com/android/launcher3/ExtendedEditText.java b/src/com/android/launcher3/ExtendedEditText.java
index 98c71c5..11f2020 100644
--- a/src/com/android/launcher3/ExtendedEditText.java
+++ b/src/com/android/launcher3/ExtendedEditText.java
@@ -16,7 +16,6 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.SHOW;
-import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync;
 
 import android.content.Context;
 import android.text.TextUtils;
@@ -90,7 +89,7 @@
     }
 
     public void hideKeyboard() {
-        hideKeyboardAsync(ActivityContext.lookupContext(getContext()), getWindowToken());
+        ActivityContext.lookupContext(getContext()).hideKeyboard();
         clearFocus();
     }
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8e978d1..6bdfa1c 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -194,7 +194,6 @@
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.util.TraceHelper;
-import com.android.launcher3.util.UiThreadHelper;
 import com.android.launcher3.util.ViewOnDrawExecutor;
 import com.android.launcher3.views.ActivityContext;
 import com.android.launcher3.views.FloatingIconView;
@@ -1654,16 +1653,6 @@
         }
     }
 
-    /**
-     * Hides the keyboard if visible
-     */
-    public void hideKeyboard() {
-        final View v = getWindow().peekDecorView();
-        if (v != null && v.getWindowToken() != null) {
-            UiThreadHelper.hideKeyboardAsync(this, v.getWindowToken());
-        }
-    }
-
     @Override
     public void onRestoreInstanceState(Bundle state) {
         super.onRestoreInstanceState(state);
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index e33e44e..aa9cfd1 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.allapps.BaseAllAppsContainerView.AdapterHolder.SEARCH;
+
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
@@ -34,7 +36,6 @@
 import com.android.launcher3.views.AppLauncher;
 
 import java.util.ArrayList;
-import java.util.Objects;
 
 /**
  * All apps container view with search support for use in a dragging activity.
@@ -44,6 +45,11 @@
 public class ActivityAllAppsContainerView<T extends Context & AppLauncher
         & DeviceProfileListenable> extends BaseAllAppsContainerView<T> {
 
+    private static final long DEFAULT_SEARCH_TRANSITION_DURATION_MS = 300;
+
+    // Used to animate Search results out and A-Z apps in, or vice-versa.
+    private final SearchTransitionController mSearchTransitionController;
+
     protected SearchUiManager mSearchUiManager;
     /**
      * View that defines the search box. Result is rendered inside the recycler view defined in the
@@ -52,6 +58,7 @@
     private View mSearchContainer;
     /** {@code true} when rendered view is in search state instead of the scroll state. */
     private boolean mIsSearching;
+    private boolean mRebindAdaptersAfterSearchAnimation;
 
     public ActivityAllAppsContainerView(Context context) {
         this(context, null);
@@ -63,6 +70,8 @@
 
     public ActivityAllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+
+        mSearchTransitionController = new SearchTransitionController(this);
     }
 
     public SearchUiManager getSearchUiManager() {
@@ -73,19 +82,10 @@
         return mSearchContainer;
     }
 
-    /** Updates all apps container with the latest search query. */
-    public void setLastSearchQuery(String query) {
-        mIsSearching = true;
-        rebindAdapters();
-        mHeader.setCollapsed(true);
-    }
-
     /** Invoke when the current search session is finished. */
     public void onClearSearchResult() {
-        mIsSearching = false;
-        mHeader.setCollapsed(false);
+        animateToSearchState(false);
         rebindAdapters();
-        mHeader.reset(false);
     }
 
     /**
@@ -93,12 +93,42 @@
      */
     public void setSearchResults(ArrayList<AdapterItem> results) {
         if (getSearchResultList().setSearchResults(results)) {
-            for (int i = 0; i < mAH.size(); i++) {
-                if (mAH.get(i).mRecyclerView != null) {
-                    mAH.get(i).mRecyclerView.onSearchResultsChanged();
-                }
-            }
+            getSearchRecyclerView().onSearchResultsChanged();
         }
+        if (results != null) {
+            animateToSearchState(true);
+        }
+    }
+
+    private void animateToSearchState(boolean goingToSearch) {
+        animateToSearchState(goingToSearch, DEFAULT_SEARCH_TRANSITION_DURATION_MS);
+    }
+
+    private void animateToSearchState(boolean goingToSearch, long durationMs) {
+        if (!mSearchTransitionController.isRunning() && goingToSearch == isSearching()) {
+            return;
+        }
+        if (goingToSearch) {
+            // Fade out the button to pause work apps.
+            mWorkManager.onActivePageChanged(SEARCH);
+        }
+        mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
+                /* onEndRunnable = */ () -> {
+                    mIsSearching = goingToSearch;
+                    updateSearchResultsVisibility();
+                    int previousPage = getCurrentPage();
+                    if (mRebindAdaptersAfterSearchAnimation) {
+                        rebindAdapters(false);
+                        mRebindAdaptersAfterSearchAnimation = false;
+                    }
+                    if (!goingToSearch) {
+                        setSearchResults(null);
+                        if (mViewPager != null) {
+                            mViewPager.setCurrentPage(previousPage);
+                        }
+                        onActivePageChanged(previousPage);
+                    }
+                });
     }
 
     @Override
@@ -121,6 +151,8 @@
         super.reset(animate);
         // Reset the search bar after transitioning home.
         mSearchUiManager.resetSearch();
+        // Animate to A-Z with 0 time to reset the animation with proper state management.
+        animateToSearchState(false, 0);
     }
 
     @Override
@@ -157,21 +189,30 @@
     }
 
     @Override
+    public void onActivePageChanged(int currentActivePage) {
+        if (mSearchTransitionController.isRunning()) {
+            // Will be called at the end of the animation.
+            return;
+        }
+        super.onActivePageChanged(currentActivePage);
+    }
+
+    @Override
     protected void rebindAdapters(boolean force) {
+        if (mSearchTransitionController.isRunning()) {
+            mRebindAdaptersAfterSearchAnimation = true;
+            return;
+        }
         super.rebindAdapters(force);
         if (!FeatureFlags.ENABLE_DEVICE_SEARCH.get()
-                || getMainAdapterProvider().getDecorator() == null) {
+                || getMainAdapterProvider().getDecorator() == null
+                || getSearchRecyclerView() == null) {
             return;
         }
 
         RecyclerView.ItemDecoration decoration = getMainAdapterProvider().getDecorator();
-        mAH.stream()
-                .map(adapterHolder -> adapterHolder.mRecyclerView)
-                .filter(Objects::nonNull)
-                .forEach(v -> {
-                    v.removeItemDecoration(decoration); // Remove in case it is already added.
-                    v.addItemDecoration(decoration);
-                });
+        getSearchRecyclerView().removeItemDecoration(decoration); // In case it is already added.
+        getSearchRecyclerView().addItemDecoration(decoration);
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 31e3890..21a7dfb 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -21,7 +21,6 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_VERTICAL_SWIPE_END;
 import static com.android.launcher3.util.LogConfig.SEARCH_LOGGING;
-import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync;
 
 import android.content.Context;
 import android.graphics.Canvas;
@@ -162,8 +161,7 @@
                 requestFocus();
                 mgr.logger().sendToInteractionJankMonitor(
                         LAUNCHER_ALLAPPS_VERTICAL_SWIPE_BEGIN, this);
-                hideKeyboardAsync(ActivityContext.lookupContext(getContext()),
-                        getApplicationWindowToken());
+                ActivityContext.lookupContext(getContext()).hideKeyboard();
                 break;
             case SCROLL_STATE_IDLE:
                 mgr.logger().sendToInteractionJankMonitor(
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 6990e57..42f8b0c 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -127,6 +127,11 @@
         public boolean isContentSame(AdapterItem other) {
             return itemInfo == null && other.itemInfo == null;
         }
+
+        /** Sets the alpha of the decorator for this item. Returns true if successful. */
+        public boolean setDecorationFillAlpha(int alpha) {
+            return false;
+        }
     }
 
     protected final T mActivityContext;
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
index f3c5dd6..879a159 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
@@ -17,7 +17,6 @@
 
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB;
-import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -107,12 +106,13 @@
                     updateHeaderScroll(((AllAppsRecyclerView) recyclerView).getCurrentScrollY());
                 }
             };
-    private final WorkProfileManager mWorkManager;
+
+    protected final WorkProfileManager mWorkManager;
 
     private final Paint mNavBarScrimPaint;
     private int mNavBarScrimHeight = 0;
 
-    private AllAppsPagedView mViewPager;
+    protected AllAppsPagedView mViewPager;
     private SearchRecyclerView mSearchRecyclerView;
 
     protected FloatingHeaderView mHeader;
@@ -349,7 +349,7 @@
      * The container for A-Z apps (the ViewPager for main+work tabs, or main RV). This is currently
      * hidden while searching.
      **/
-    private View getAppsRecyclerViewContainer() {
+    protected View getAppsRecyclerViewContainer() {
         return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
     }
 
@@ -503,8 +503,7 @@
                             mActivityContext.getStatsLogManager().logger()
                                     .log(LAUNCHER_ALLAPPS_TAP_ON_PERSONAL_TAB);
                         }
-                        hideKeyboardAsync(ActivityContext.lookupContext(getContext()),
-                                getApplicationWindowToken());
+                        mActivityContext.hideKeyboard();
                     });
             findViewById(R.id.tab_work)
                     .setOnClickListener((View view) -> {
@@ -512,8 +511,7 @@
                             mActivityContext.getStatsLogManager().logger()
                                     .log(LAUNCHER_ALLAPPS_TAP_ON_WORK_TAB);
                         }
-                        hideKeyboardAsync(ActivityContext.lookupContext(getContext()),
-                                getApplicationWindowToken());
+                        mActivityContext.hideKeyboard();
                     });
             setDeviceManagementResources();
             onActivePageChanged(mViewPager.getNextPage());
@@ -527,7 +525,7 @@
         mAllAppsStore.registerIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
     }
 
-    private void updateSearchResultsVisibility() {
+    protected void updateSearchResultsVisibility() {
         if (isSearching()) {
             getSearchRecyclerView().setVisibility(VISIBLE);
             getAppsRecyclerViewContainer().setVisibility(GONE);
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index c5bdb69..8ec2aeb 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -42,6 +42,7 @@
 import com.android.systemui.plugins.PluginListener;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Map;
 
 public class FloatingHeaderView extends LinearLayout implements
@@ -82,8 +83,8 @@
     protected final Map<AllAppsRow, PluginHeaderRow> mPluginRows = new ArrayMap<>();
 
     // These two values are necessary to ensure that the header protection is drawn correctly.
-    private final int mHeaderTopAdjustment;
-    private final int mHeaderBottomAdjustment;
+    private final int mTabsAdditionalPaddingTop;
+    private final int mTabsAdditionalPaddingBottom;
     private boolean mHeaderProtectionSupported;
 
     protected ViewGroup mTabLayout;
@@ -91,7 +92,6 @@
     private AllAppsRecyclerView mWorkRV;
     private SearchRecyclerView mSearchRV;
     private AllAppsRecyclerView mCurrentRV;
-    public boolean mHeaderCollapsed;
     protected int mSnappedScrolledY;
     private int mTranslationY;
 
@@ -100,7 +100,12 @@
     protected boolean mTabsHidden;
     protected int mMaxTranslation;
 
-    private boolean mCollapsed = false;
+    // Whether the header has been scrolled off-screen.
+    private boolean mHeaderCollapsed;
+    // Whether floating rows like predicted apps are hidden.
+    private boolean mFloatingRowsCollapsed;
+    // Total height of all current floating rows. Collapsed rows == 0 height.
+    private int mFloatingRowsHeight;
 
     // This is initialized once during inflation and stays constant after that. Fixed views
     // cannot be added or removed dynamically.
@@ -117,9 +122,9 @@
 
     public FloatingHeaderView(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
-        mHeaderTopAdjustment = context.getResources()
+        mTabsAdditionalPaddingTop = context.getResources()
                 .getDimensionPixelSize(R.dimen.all_apps_header_top_adjustment);
-        mHeaderBottomAdjustment = context.getResources()
+        mTabsAdditionalPaddingBottom = context.getResources()
                 .getDimensionPixelSize(R.dimen.all_apps_header_bottom_adjustment);
         mHeaderProtectionSupported = context.getResources().getBoolean(
                 R.bool.config_header_protection_supported);
@@ -148,6 +153,7 @@
         }
         mFixedRows = rows.toArray(new FloatingHeaderRow[rows.size()]);
         mAllRows = mFixedRows;
+        updateFloatingRowsHeight();
     }
 
     @Override
@@ -179,6 +185,7 @@
                 count++;
             }
         }
+        updateFloatingRowsHeight();
     }
 
     @Override
@@ -195,7 +202,7 @@
         int oldMaxHeight = mMaxTranslation;
         updateExpectedHeight();
 
-        if (mMaxTranslation != oldMaxHeight || mCollapsed) {
+        if (mMaxTranslation != oldMaxHeight || mFloatingRowsCollapsed) {
             BaseAllAppsContainerView<?> parent = (BaseAllAppsContainerView<?>) getParent();
             if (parent != null) {
                 parent.setupHeader();
@@ -258,20 +265,19 @@
     }
 
     private void updateExpectedHeight() {
+        updateFloatingRowsHeight();
         mMaxTranslation = 0;
-        if (mCollapsed) {
+        if (mFloatingRowsCollapsed) {
             return;
         }
-        for (FloatingHeaderRow row : mAllRows) {
-            mMaxTranslation += row.getExpectedHeight();
-        }
+        mMaxTranslation += mFloatingRowsHeight;
         if (!mTabsHidden) {
-            mMaxTranslation += mHeaderBottomAdjustment;
+            mMaxTranslation += mTabsAdditionalPaddingBottom;
         }
     }
 
     public int getMaxTranslation() {
-        if (mMaxTranslation == 0 && mTabsHidden) {
+        if (mMaxTranslation == 0 && (mTabsHidden || mFloatingRowsCollapsed)) {
             return getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_bottom_padding);
         } else if (mMaxTranslation > 0 && mTabsHidden) {
             return mMaxTranslation + getPaddingTop();
@@ -312,7 +318,7 @@
         int uncappedTranslationY = mTranslationY;
         mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
 
-        if (mCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) {
+        if (mFloatingRowsCollapsed || uncappedTranslationY < mTranslationY - getPaddingTop()) {
             // we hide it completely if already capped (for opening search anim)
             for (FloatingHeaderRow row : mAllRows) {
                 row.setVerticalScroll(0, true /* isScrolledOut */);
@@ -325,11 +331,11 @@
 
         mTabLayout.setTranslationY(mTranslationY);
 
-        int clipTop = getPaddingTop() - mHeaderTopAdjustment;
+        int clipTop = getPaddingTop() - mTabsAdditionalPaddingTop;
         if (mTabsHidden) {
-            clipTop += getPaddingBottom() - mHeaderBottomAdjustment;
+            clipTop += getPaddingBottom() - mTabsAdditionalPaddingBottom;
         }
-        mRVClip.top = mTabsHidden ? clipTop : 0;
+        mRVClip.top = mTabsHidden || mFloatingRowsCollapsed ? clipTop : 0;
         mHeaderClip.top = clipTop;
         // clipping on a draw might cause additional redraw
         setClipBounds(mHeaderClip);
@@ -347,10 +353,12 @@
     /**
      * Hides all the floating rows
      */
-    public void setCollapsed(boolean collapse) {
-        if (mCollapsed == collapse) return;
+    public void setFloatingRowsCollapsed(boolean collapsed) {
+        if (mFloatingRowsCollapsed == collapsed) {
+            return;
+        }
 
-        mCollapsed = collapse;
+        mFloatingRowsCollapsed = collapsed;
         onHeightUpdated();
     }
 
@@ -376,6 +384,30 @@
         return !mHeaderCollapsed;
     }
 
+    /** Returns true if personal/work tabs are currently in use. */
+    public boolean usingTabs() {
+        return !mTabsHidden;
+    }
+
+    ViewGroup getTabLayout() {
+        return mTabLayout;
+    }
+
+    /** Calculates the combined height of any floating rows (e.g. predicted apps, app divider). */
+    private void updateFloatingRowsHeight() {
+        mFloatingRowsHeight =
+                Arrays.stream(mAllRows).mapToInt(FloatingHeaderRow::getExpectedHeight).sum();
+    }
+
+    /** Gets the combined height of any floating rows (e.g. predicted apps, app divider). */
+    int getFloatingRowsHeight() {
+        return mFloatingRowsHeight;
+    }
+
+    int getTabsAdditionalPaddingBottom() {
+        return mTabsAdditionalPaddingBottom;
+    }
+
     @Override
     public void onAnimationUpdate(ValueAnimator animation) {
         mTranslationY = (Integer) animation.getAnimatedValue();
@@ -447,9 +479,10 @@
 
         // we only want to show protection when work tab is available and header is either
         // collapsed or animating to/from collapsed state
-        if (mTabsHidden || !mHeaderCollapsed) {
+        if (mTabsHidden || mFloatingRowsCollapsed || !mHeaderCollapsed) {
             return 0;
         }
-        return Math.max(getHeight() - getPaddingTop() + mTranslationY, 0);
+        return Math.max(0,
+                getTabLayout().getBottom() - getPaddingTop() + getPaddingBottom() + mTranslationY);
     }
 }
diff --git a/src/com/android/launcher3/allapps/SearchRecyclerView.java b/src/com/android/launcher3/allapps/SearchRecyclerView.java
index 482bd29..9d1dfc0 100644
--- a/src/com/android/launcher3/allapps/SearchRecyclerView.java
+++ b/src/com/android/launcher3/allapps/SearchRecyclerView.java
@@ -17,12 +17,17 @@
 
 import android.content.Context;
 import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
 
 import com.android.launcher3.views.RecyclerViewFastScroller;
 
 /** A RecyclerView for AllApps Search results. */
 public class SearchRecyclerView extends AllAppsRecyclerView {
-    private static final String TAG = "SearchRecyclerView";
+
+    private Consumer<View> mChildAttachedConsumer;
 
     public SearchRecyclerView(Context context) {
         this(context, null);
@@ -41,6 +46,11 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
+    /** This will be called just before a new child is attached to the window. */
+    public void setChildAttachedConsumer(Consumer<View> childAttachedConsumer) {
+        mChildAttachedConsumer = childAttachedConsumer;
+    }
+
     @Override
     protected void updatePoolSize() {
         RecycledViewPool pool = getRecycledViewPool();
@@ -57,4 +67,12 @@
     public RecyclerViewFastScroller getScrollbar() {
         return null;
     }
+
+    @Override
+    public void onChildAttachedToWindow(@NonNull View child) {
+        if (mChildAttachedConsumer != null) {
+            mChildAttachedConsumer.accept(child);
+        }
+        super.onChildAttachedToWindow(child);
+    }
 }
diff --git a/src/com/android/launcher3/allapps/SearchTransitionController.java b/src/com/android/launcher3/allapps/SearchTransitionController.java
new file mode 100644
index 0000000..03529af
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SearchTransitionController.java
@@ -0,0 +1,258 @@
+/*
+ * 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.allapps;
+
+import static android.view.View.VISIBLE;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+
+import android.animation.ObjectAnimator;
+import android.graphics.drawable.Drawable;
+import android.util.FloatProperty;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+
+/** Coordinates the transition between Search and A-Z in All Apps. */
+public class SearchTransitionController {
+
+    // Interpolator when the user taps the QSB while already in All Apps.
+    private static final Interpolator DEFAULT_INTERPOLATOR_WITHIN_ALL_APPS = DEACCEL_1_7;
+    // Interpolator when the user taps the QSB from home screen, so transition to all apps is
+    // happening simultaneously.
+    private static final Interpolator DEFAULT_INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = LINEAR;
+
+    /**
+     * These values represent points on the [0, 1] animation progress spectrum. They are used to
+     * animate items in the {@link SearchRecyclerView}.
+     */
+    private static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
+    private static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
+    private static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
+    private static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
+    private static final float CONTENT_STAGGER = 0.01f;  // Progress before next item starts fading.
+
+    private static final FloatProperty<SearchTransitionController> SEARCH_TO_AZ_PROGRESS =
+            new FloatProperty<SearchTransitionController>("searchToAzProgress") {
+                @Override
+                public Float get(SearchTransitionController controller) {
+                    return controller.getSearchToAzProgress();
+                }
+
+                @Override
+                public void setValue(SearchTransitionController controller, float progress) {
+                    controller.setSearchToAzProgress(progress);
+                }
+            };
+
+    private final ActivityAllAppsContainerView<?> mAllAppsContainerView;
+
+    private ObjectAnimator mSearchToAzAnimator = null;
+    private float mSearchToAzProgress = 1f;
+
+    public SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView) {
+        mAllAppsContainerView = allAppsContainerView;
+    }
+
+    /** Returns true if a transition animation is currently in progress. */
+    public boolean isRunning() {
+        return mSearchToAzAnimator != null;
+    }
+
+    /**
+     * Starts the transition to or from search state. If a transition is already in progress, the
+     * animation will start from that point with the new duration, and the previous onEndRunnable
+     * will not be called.
+     *
+     * @param goingToSearch true if will be showing search results, otherwise will be showing a-z
+     * @param duration time in ms for the animation to run
+     * @param onEndRunnable will be called when the animation finishes, unless another animation is
+     *                      scheduled in the meantime
+     */
+    public void animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
+        float targetProgress = goingToSearch ? 0 : 1;
+
+        if (mSearchToAzAnimator != null) {
+            mSearchToAzAnimator.cancel();
+        }
+
+        mSearchToAzAnimator = ObjectAnimator.ofFloat(this, SEARCH_TO_AZ_PROGRESS, targetProgress);
+        boolean inAllApps = Launcher.getLauncher(
+                mAllAppsContainerView.getContext()).getStateManager().isInStableState(
+                LauncherState.ALL_APPS);
+        mSearchToAzAnimator.setDuration(duration).setInterpolator(
+                inAllApps ? DEFAULT_INTERPOLATOR_WITHIN_ALL_APPS
+                        : DEFAULT_INTERPOLATOR_TRANSITIONING_TO_ALL_APPS);
+        mSearchToAzAnimator.addListener(forEndCallback(() -> mSearchToAzAnimator = null));
+        if (!goingToSearch) {
+            mSearchToAzAnimator.addListener(forSuccessCallback(() -> {
+                mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(false);
+                mAllAppsContainerView.getFloatingHeaderView().reset(false /* animate */);
+                mAllAppsContainerView.getAppsRecyclerViewContainer().setTranslationY(0);
+            }));
+        }
+        mSearchToAzAnimator.addListener(forSuccessCallback(onEndRunnable));
+
+        mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(true);
+        mAllAppsContainerView.getAppsRecyclerViewContainer().setVisibility(VISIBLE);
+        getSearchRecyclerView().setVisibility(VISIBLE);
+        getSearchRecyclerView().setChildAttachedConsumer(this::onSearchChildAttached);
+        mSearchToAzAnimator.start();
+    }
+
+    private SearchRecyclerView getSearchRecyclerView() {
+        return mAllAppsContainerView.getSearchRecyclerView();
+    }
+
+    private void setSearchToAzProgress(float searchToAzProgress) {
+        mSearchToAzProgress = searchToAzProgress;
+        int searchHeight = updateSearchRecyclerViewProgress();
+
+        FloatingHeaderView headerView = mAllAppsContainerView.getFloatingHeaderView();
+
+        // Add predictions + app divider height to account for predicted apps which will now be in
+        // the Search RV instead of the floating header view. Note `getFloatingRowsHeight` returns 0
+        // when predictions are not shown.
+        int appsTranslationY = searchHeight + headerView.getFloatingRowsHeight();
+
+        if (headerView.usingTabs()) {
+            // Move tabs below the search results, and fade them out in 20% of the animation.
+            headerView.setTranslationY(searchHeight);
+            headerView.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
+
+            // Account for the additional padding added for the tabs.
+            appsTranslationY -=
+                    headerView.getPaddingTop() - headerView.getTabsAdditionalPaddingBottom();
+        }
+
+        View appsContainer = mAllAppsContainerView.getAppsRecyclerViewContainer();
+        appsContainer.setTranslationY(appsTranslationY);
+        // Fade apps out with tabs (in 20% of the total animation).
+        appsContainer.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
+    }
+
+    /**
+     * Updates the children views of SearchRecyclerView based on the current animation progress.
+     *
+     * @return the total height of animating views (excluding any app icons).
+     */
+    private int updateSearchRecyclerViewProgress() {
+        int numSearchResultsAnimated = 0;
+        int totalHeight = 0;
+        int appRowHeight = 0;
+        Integer top = null;
+        SearchRecyclerView searchRecyclerView = getSearchRecyclerView();
+        for (int i = 0; i < searchRecyclerView.getChildCount(); i++) {
+            View searchResultView = searchRecyclerView.getChildAt(i);
+            if (searchResultView == null) {
+                continue;
+            }
+
+            if (top == null) {
+                top = searchResultView.getTop();
+            }
+
+            if (searchResultView instanceof BubbleTextView) {
+                // The first app icon will set appRowHeight, which will also contribute to
+                // totalHeight. Additional app icons should remove the appRowHeight to remain in
+                // the same row as the first app.
+                searchResultView.setY(top + totalHeight - appRowHeight);
+                if (appRowHeight == 0) {
+                    appRowHeight = searchResultView.getHeight();
+                    totalHeight += appRowHeight;
+                }
+                // Don't scale/fade app row.
+                continue;
+            }
+
+            // Adjust content alpha based on start progress and stagger.
+            float startContentFadeProgress = Math.max(0,
+                    TOP_CONTENT_FADE_PROGRESS_START - CONTENT_STAGGER * numSearchResultsAnimated);
+            float endContentFadeProgress = Math.min(1,
+                    startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
+            searchResultView.setAlpha(1 - clampToProgress(mSearchToAzProgress,
+                    startContentFadeProgress, endContentFadeProgress));
+
+            // Adjust background (or decorator) alpha based on start progress and stagger.
+            float startBackgroundFadeProgress = Math.max(0,
+                    TOP_BACKGROUND_FADE_PROGRESS_START
+                            - CONTENT_STAGGER * numSearchResultsAnimated);
+            float endBackgroundFadeProgress = Math.min(1,
+                    startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
+            float backgroundAlpha = 1 - clampToProgress(mSearchToAzProgress,
+                    startBackgroundFadeProgress, endBackgroundFadeProgress);
+            int adapterPosition = searchRecyclerView.getChildAdapterPosition(searchResultView);
+            boolean decoratorFilled =
+                    adapterPosition != NO_POSITION
+                            && searchRecyclerView.getApps().getAdapterItems().get(adapterPosition)
+                            .setDecorationFillAlpha((int) (255 * backgroundAlpha));
+            if (!decoratorFilled) {
+                // Try to adjust background alpha instead (e.g. for Search Edu card).
+                Drawable background = searchResultView.getBackground();
+                if (background != null) {
+                    background.setAlpha((int) (255 * backgroundAlpha));
+                }
+            }
+
+            float scaleY = 1 - mSearchToAzProgress;
+            int scaledHeight = (int) (searchResultView.getHeight() * scaleY);
+            searchResultView.setScaleY(scaleY);
+            searchResultView.setY(top + totalHeight);
+
+            numSearchResultsAnimated++;
+            totalHeight += scaledHeight;
+        }
+
+        return totalHeight - appRowHeight;
+    }
+
+    /** Called just before a child is attached to the SearchRecyclerView. */
+    private void onSearchChildAttached(View child) {
+        // Avoid allocating hardware layers for alpha changes.
+        child.forceHasOverlappingRendering(false);
+        if (mSearchToAzProgress > 0) {
+            // Before the child is rendered, apply the animation including it to avoid flicker.
+            updateSearchRecyclerViewProgress();
+        } else {
+            // Apply default states without processing the full layout.
+            child.setAlpha(1);
+            child.setScaleY(1);
+            child.setTranslationY(0);
+            int adapterPosition = getSearchRecyclerView().getChildAdapterPosition(child);
+            if (adapterPosition != NO_POSITION) {
+                getSearchRecyclerView().getApps().getAdapterItems().get(adapterPosition)
+                        .setDecorationFillAlpha(255);
+            }
+            if (child.getBackground() != null) {
+                child.getBackground().setAlpha(255);
+            }
+        }
+    }
+
+    private float getSearchToAzProgress() {
+        return mSearchToAzProgress;
+    }
+}
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index 52d8f63..78c305b 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -168,14 +168,11 @@
     public void onSearchResult(String query, ArrayList<AdapterItem> items) {
         if (items != null) {
             mAppsView.setSearchResults(items);
-            mAppsView.setLastSearchQuery(query);
         }
     }
 
     @Override
     public void clearSearchResult() {
-        mAppsView.setSearchResults(null);
-
         // Clear the search query
         mSearchQueryBuilder.clear();
         mSearchQueryBuilder.clearSpans();
diff --git a/src/com/android/launcher3/util/UiThreadHelper.java b/src/com/android/launcher3/util/UiThreadHelper.java
index 013479f..706b51f 100644
--- a/src/com/android/launcher3/util/UiThreadHelper.java
+++ b/src/com/android/launcher3/util/UiThreadHelper.java
@@ -15,24 +15,12 @@
  */
 package com.android.launcher3.util;
 
-import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
-import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Context;
-import android.os.Bundle;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Message;
-import android.view.View;
-import android.view.WindowInsets;
-import android.view.WindowInsetsController;
-import android.view.inputmethod.InputMethodManager;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.views.ActivityContext;
 
 /**
  * Utility class for offloading some class from UI thread
@@ -43,54 +31,8 @@
             new MainThreadInitializedObject<>(
                     c -> new Handler(UI_HELPER_EXECUTOR.getLooper(), new UiCallbacks(c)));
 
-    private static final int MSG_HIDE_KEYBOARD = 1;
-    private static final int MSG_SET_ORIENTATION = 2;
-    private static final int MSG_RUN_COMMAND = 3;
-    private static final String STATS_LOGGER_KEY = "STATS_LOGGER_KEY";
-
-    @SuppressLint("NewApi")
-    public static void hideKeyboardAsync(ActivityContext activityContext, IBinder token) {
-        View root = activityContext.getDragLayer();
-
-        if (Utilities.ATLEAST_R) {
-            Preconditions.assertUIThread();
-            //  Hide keyboard with WindowInsetsController if could. In case
-            //  hideSoftInputFromWindow may get ignored by input connection being finished
-            //  when the screen is off.
-            //
-            // In addition, inside IMF, the keyboards are closed asynchronously that launcher no
-            // longer need to post to the message queue.
-            final WindowInsetsController wic = root.getWindowInsetsController();
-            WindowInsets insets = root.getRootWindowInsets();
-            boolean isImeShown = insets != null && insets.isVisible(WindowInsets.Type.ime());
-            if (wic != null && isImeShown) {
-                activityContext.getStatsLogManager().keyboardStateManager().setKeyboardState(HIDE);
-                // this method cannot be called cross threads
-                wic.hide(WindowInsets.Type.ime());
-                activityContext.getStatsLogManager().logger()
-                        .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
-                return;
-            }
-        }
-        // Since the launcher context cannot be accessed directly from callback, adding secondary
-        // message to log keyboard close event asynchronously.
-        Bundle mHideKeyboardLoggerMsg = new Bundle();
-        mHideKeyboardLoggerMsg.putParcelable(
-                STATS_LOGGER_KEY,
-                Message.obtain(
-                        HANDLER.get(root.getContext()),
-                        () -> activityContext
-                                .getStatsLogManager()
-                                .logger()
-                                .log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED)
-                )
-        );
-
-        Message mHideKeyboardMsg = Message.obtain(HANDLER.get(root.getContext()), MSG_HIDE_KEYBOARD,
-                token);
-        mHideKeyboardMsg.setData(mHideKeyboardLoggerMsg);
-        mHideKeyboardMsg.sendToTarget();
-    }
+    private static final int MSG_SET_ORIENTATION = 1;
+    private static final int MSG_RUN_COMMAND = 2;
 
     public static void setOrientationAsync(Activity activity, int orientation) {
         Message.obtain(HANDLER.get(activity), MSG_SET_ORIENTATION, orientation, 0, activity)
@@ -104,23 +46,14 @@
     private static class UiCallbacks implements Handler.Callback {
 
         private final Context mContext;
-        private final InputMethodManager mIMM;
 
         UiCallbacks(Context context) {
             mContext = context;
-            mIMM = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
         }
 
         @Override
         public boolean handleMessage(Message message) {
             switch (message.what) {
-                case MSG_HIDE_KEYBOARD:
-                    if (mIMM.hideSoftInputFromWindow((IBinder) message.obj, 0)) {
-                        // log keyboard close event only when keyboard is actually closed
-                        ((Message) message.getData().getParcelable(STATS_LOGGER_KEY))
-                                .sendToTarget();
-                    }
-                    return true;
                 case MSG_SET_ORIENTATION:
                     ((Activity) message.obj).setRequestedOrientation(message.arg1);
                     return true;
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index e2dc34f..dd5b22e 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -15,16 +15,26 @@
  */
 package com.android.launcher3.views;
 
+import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import android.content.Context;
 import android.content.ContextWrapper;
 import android.graphics.Rect;
+import android.os.IBinder;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.allapps.search.SearchAdapterProvider;
 import com.android.launcher3.dot.DotInfo;
@@ -36,6 +46,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.ViewCache;
 
 /**
@@ -202,4 +213,47 @@
             ActivityAllAppsContainerView<?> appsView) {
         return null;
     }
+
+    /**
+     * Hides the keyboard if it is visible
+     */
+    default void hideKeyboard() {
+        View root = getDragLayer();
+        if (root == null) {
+            return;
+        }
+        if (Utilities.ATLEAST_R) {
+            Preconditions.assertUIThread();
+            //  Hide keyboard with WindowInsetsController if could. In case
+            //  hideSoftInputFromWindow may get ignored by input connection being finished
+            //  when the screen is off.
+            //
+            // In addition, inside IMF, the keyboards are closed asynchronously that launcher no
+            // longer need to post to the message queue.
+            final WindowInsetsController wic = root.getWindowInsetsController();
+            WindowInsets insets = root.getRootWindowInsets();
+            boolean isImeShown = insets != null && insets.isVisible(WindowInsets.Type.ime());
+            if (wic != null && isImeShown) {
+                StatsLogManager slm  = getStatsLogManager();
+                slm.keyboardStateManager().setKeyboardState(HIDE);
+
+                // this method cannot be called cross threads
+                wic.hide(WindowInsets.Type.ime());
+                slm.logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED);
+                return;
+            }
+        }
+
+        InputMethodManager imm = root.getContext().getSystemService(InputMethodManager.class);
+        IBinder token = root.getWindowToken();
+        if (imm != null && token != null) {
+            UI_HELPER_EXECUTOR.execute(() -> {
+                if (imm.hideSoftInputFromWindow(token, 0)) {
+                    // log keyboard close event only when keyboard is actually closed
+                    MAIN_EXECUTOR.execute(() ->
+                            getStatsLogManager().logger().log(LAUNCHER_ALLAPPS_KEYBOARD_CLOSED));
+                }
+            });
+        }
+    }
 }
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 6e6512d..40e4ce1 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -20,8 +20,6 @@
 
 import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
 
-import static com.android.launcher3.util.UiThreadHelper.hideKeyboardAsync;
-
 import android.animation.ObjectAnimator;
 import android.content.Context;
 import android.content.res.Resources;
@@ -313,7 +311,7 @@
     }
 
     private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) {
-        hideKeyboardAsync(ActivityContext.lookupContext(getContext()), getWindowToken());
+        ActivityContext.lookupContext(getContext()).hideKeyboard();
         mIsDragging = true;
         if (mCanThumbDetach) {
             mIsThumbDetached = true;
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 5baa7d2..be49974 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -214,9 +214,9 @@
         // Test that ensureWorkspaceIsScrollable adds a page by dragging an icon there.
         executeOnLauncher(launcher -> assertFalse("Initial workspace state is scrollable",
                 isWorkspaceScrollable(launcher)));
-        workspace.verifyWorkspaceAppIconIsGone(
-                "Chrome app was found on empty workspace", "Chrome");
-
+        assertEquals("Initial workspace doesn't have the correct page", workspace.pagesPerScreen(),
+                workspace.getPageCount());
+        workspace.verifyWorkspaceAppIconIsGone("Chrome app was found on empty workspace", "Chrome");
         workspace.ensureWorkspaceIsScrollable();
 
         executeOnLauncher(
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
index 5757f13..302bd2f 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkProfileTest.java
@@ -22,6 +22,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
+
 import android.util.Log;
 import android.view.View;
 
@@ -135,7 +136,11 @@
                 LauncherInstrumentation.WAIT_TIME_MS);
 
         //start work profile toggle OFF test
-        executeOnLauncher(l -> l.getAppsView().getWorkManager().getWorkModeSwitch().performClick());
+        executeOnLauncher(l -> {
+            // Ensure updates are not deferred so notification happens when apps pause.
+            l.getAppsView().getAppsStore().disableDeferUpdates(DEFER_UPDATES_TEST);
+            l.getAppsView().getWorkManager().getWorkModeSwitch().performClick();
+        });
 
         waitForLauncherCondition("Work profile toggle OFF failed", launcher -> {
             manager.reset(); // pulls current state from system
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index efbdb23..5fab7eb 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -192,6 +192,12 @@
         }
     }
 
+    /** Returns the number of pages. */
+    public int getPageCount() {
+        final UiObject2 workspace = verifyActiveContainer();
+        return workspace.getChildCount();
+    }
+
     /**
      * Returns the number of pages that are visible on the screen simultaneously.
      */