Merge "Merging from ub-launcher3-master @ build 6682763"
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 84dd06a..19a16e3 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -45,9 +45,6 @@
     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
-
-    <!-- TODO(b/150802536): Enabled only for ENABLE_FIXED_ROTATION_TRANSFORM feature flag -->
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
     
     <!--
     Permissions required for read/write access to the workspace data. These permission name
diff --git a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
index e649ce1..aa3710b 100644
--- a/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
+++ b/ext_tests/src/com/android/launcher3/testing/DebugTestInformationHandler.java
@@ -164,6 +164,12 @@
             }
 
             case TestProtocol.REQUEST_GET_TEST_EVENTS: {
+                if (sEvents == null) {
+                    // sEvents can be null if Launcher died and restarted after
+                    // REQUEST_START_EVENT_LOGGING.
+                    return response;
+                }
+
                 synchronized (sEvents) {
                     response.putStringArrayList(
                             TestProtocol.TEST_INFO_RESPONSE_FIELD, new ArrayList<>(sEvents));
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 1a634ce..53910e3 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -53,8 +53,8 @@
             android:resizeableActivity="true"
             android:resumeWhilePausing="true"
             android:taskAffinity=""
-            android:enabled="true"
-            android:exported="true">
+            android:exported="true"
+            android:enabled="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.HOME" />
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 868c659..4e7c3fa 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -22,14 +22,14 @@
      xmlns:tools="http://schemas.android.com/tools"
      package="com.android.launcher3">
 
-    <permission
+     <permission
         android:name="${packageName}.permission.HOTSEAT_EDU"
         android:permissionGroup="android.permission-group.SYSTEM_TOOLS"
         android:protectionLevel="signatureOrSystem" />
 
-    <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
-    <uses-permission android:name="android.permission.VIBRATE" />
-    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
     <uses-permission android:name="${packageName}.permission.HOTSEAT_EDU" />
 
     <application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
@@ -94,12 +94,11 @@
              android:clearTaskOnLaunch="true"
              android:exported="false"/>
 
-        <activity
-            android:name="com.android.quickstep.interaction.GestureSandboxActivity"
-            android:autoRemoveFromRecents="true"
-            android:excludeFromRecents="true"
-            android:screenOrientation="portrait"
-            android:exported="true">
+        <activity android:name="com.android.quickstep.interaction.GestureSandboxActivity"
+             android:autoRemoveFromRecents="true"
+             android:excludeFromRecents="true"
+             android:screenOrientation="portrait"
+             android:exported="true">
             <intent-filter>
                 <action android:name="com.android.quickstep.action.GESTURE_SANDBOX"/>
                 <category android:name="android.intent.category.DEFAULT"/>
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
index 4c7943b..e4442da 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionAppTracker.java
@@ -69,7 +69,6 @@
 
     // Accessed only on worker thread
     private AppPredictor mHomeAppPredictor;
-    private AppPredictor mRecentsOverviewPredictor;
 
     public PredictionAppTracker(Context context) {
         mContext = context;
@@ -97,10 +96,6 @@
             mHomeAppPredictor.destroy();
             mHomeAppPredictor = null;
         }
-        if (mRecentsOverviewPredictor != null) {
-            mRecentsOverviewPredictor.destroy();
-            mRecentsOverviewPredictor = null;
-        }
     }
 
     @WorkerThread
@@ -142,7 +137,6 @@
                 // Initialize the clients
                 int count = InvariantDeviceProfile.INSTANCE.get(mContext).numAllAppsColumns;
                 mHomeAppPredictor = createPredictor(Client.HOME, count);
-                mRecentsOverviewPredictor = createPredictor(Client.OVERVIEW, count);
                 return true;
             }
             case MSG_DESTROY: {
@@ -157,12 +151,7 @@
             }
             case MSG_PREDICT: {
                 if (mHomeAppPredictor != null) {
-                    String client = (String) msg.obj;
-                    if (Client.HOME.id.equals(client)) {
-                        mHomeAppPredictor.requestPredictionUpdate();
-                    } else {
-                        mRecentsOverviewPredictor.requestPredictionUpdate();
-                    }
+                    mHomeAppPredictor.requestPredictionUpdate();
                 }
                 return true;
             }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
index a0f6b04..830c103 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/appprediction/PredictionUiStateManager.java
@@ -75,8 +75,7 @@
 
     // TODO (b/129421797): Update the client constants
     public enum Client {
-        HOME("home"),
-        OVERVIEW("overview");
+        HOME("home");
 
         public final String id;
 
@@ -91,10 +90,9 @@
     private final Context mContext;
 
     private final DynamicItemCache mDynamicItemCache;
-    private final List[] mPredictionServicePredictions;
+    private List mPredictionServicePredictions = Collections.emptyList();
 
     private int mMaxIconsPerRow;
-    private Client mActiveClient;
 
     private AllAppsContainerView mAppsView;
 
@@ -108,16 +106,10 @@
 
         mDynamicItemCache = new DynamicItemCache(context, this::onAppsUpdated);
 
-        mActiveClient = Client.HOME;
-
         InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
         mMaxIconsPerRow = idp.numColumns;
 
         idp.addOnChangeListener(this);
-        mPredictionServicePredictions = new List[Client.values().length];
-        for (int i = 0; i < mPredictionServicePredictions.length; i++) {
-            mPredictionServicePredictions[i] = Collections.emptyList();
-        }
         mGettingValidPredictionResults = Utilities.getDevicePrefs(context)
                 .getBoolean(LAST_PREDICTION_ENABLED_STATE, true);
 
@@ -130,18 +122,6 @@
         mMaxIconsPerRow = profile.numColumns;
     }
 
-    public Client getClient() {
-        return mActiveClient;
-    }
-
-    public void switchClient(Client client) {
-        if (client == mActiveClient) {
-            return;
-        }
-        mActiveClient = client;
-        dispatchOnChange(true);
-    }
-
     public void setTargetAppsView(AllAppsContainerView appsView) {
         if (mAppsView != null) {
             mAppsView.getAppsStore().removeUpdateListener(this);
@@ -195,10 +175,8 @@
     }
 
     private void updatePredictionStateAfterCallback() {
-        boolean validResults = false;
-        for (List l : mPredictionServicePredictions) {
-            validResults |= l != null && !l.isEmpty();
-        }
+        boolean validResults = mPredictionServicePredictions != null
+                && !mPredictionServicePredictions.isEmpty();
         if (validResults != mGettingValidPredictionResults) {
             mGettingValidPredictionResults = validResults;
             Utilities.getDevicePrefs(mContext).edit()
@@ -210,7 +188,7 @@
 
     public AppPredictor.Callback appPredictorCallback(Client client) {
         return targets -> {
-            mPredictionServicePredictions[client.ordinal()] = targets;
+            mPredictionServicePredictions = targets;
             updatePredictionStateAfterCallback();
         };
     }
@@ -238,7 +216,7 @@
 
         state.apps = new ArrayList<>();
 
-        List<AppTarget> appTargets = mPredictionServicePredictions[mActiveClient.ordinal()];
+        List<AppTarget> appTargets = mPredictionServicePredictions;
         if (!appTargets.isEmpty()) {
             for (AppTarget appTarget : appTargets) {
                 ComponentKey key;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index de83caf..9310685 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -24,6 +24,7 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.util.Log;
 import android.view.animation.Interpolator;
 
@@ -52,17 +53,17 @@
 
     private final BaseActivityInterface<?, T> mActivityInterface;
     // The id of the currently running task that is transitioning to overview.
-    private final int mTargetTaskId;
+    private final RunningTaskInfo mTargetTask;
     private final RecentsAnimationDeviceState mDeviceState;
 
     private T mActivity;
     private RecentsView mRecentsView;
 
     AppToOverviewAnimationProvider(
-            BaseActivityInterface<?, T> activityInterface, int targetTaskId,
+            BaseActivityInterface<?, T> activityInterface, RunningTaskInfo targetTask,
             RecentsAnimationDeviceState deviceState) {
         mActivityInterface = activityInterface;
-        mTargetTaskId = targetTaskId;
+        mTargetTask = targetTask;
         mDeviceState = deviceState;
     }
 
@@ -73,7 +74,7 @@
      * @param wasVisible true if it was visible before
      */
     boolean onActivityReady(T activity, Boolean wasVisible) {
-        activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTaskId);
+        activity.<RecentsView>getOverviewPanel().showCurrentTask(mTargetTask);
         AbstractFloatingView.closeAllOpenViews(activity, wasVisible);
         BaseActivityInterface.AnimationFactory factory = mActivityInterface.prepareRecentsUI(
                 mDeviceState,
@@ -122,7 +123,8 @@
                 wallpaperTargets, MODE_CLOSING);
 
         // Use the top closing app to determine the insets for the animation
-        RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mTargetTaskId);
+        RemoteAnimationTargetCompat runningTaskTarget = mTargetTask == null ? null
+                : targets.findTask(mTargetTask.taskId);
         if (runningTaskTarget == null) {
             Log.e(TAG, "No closing app");
             return pa.buildAnim();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
index 6c4c5d3..bf03587 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandlerV2.java
@@ -38,7 +38,6 @@
 import static com.android.quickstep.GestureState.STATE_END_TARGET_SET;
 import static com.android.quickstep.GestureState.STATE_RECENTS_SCROLLING_FINISHED;
 import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
-import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
 import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -313,12 +312,6 @@
         }
 
         setupRecentsViewUi();
-
-        if (mDeviceState.getNavMode() == TWO_BUTTONS) {
-            // If the device is in two button mode, swiping up will show overview with predictions
-            // so we need to kick off switching to the overview predictions as soon as possible
-            mActivityInterface.updateOverviewPredictionState();
-        }
         linkRecentsViewScroll();
 
         return true;
@@ -423,7 +416,7 @@
     }
 
     protected void notifyGestureAnimationStartToRecents() {
-        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTaskId());
+        mRecentsView.onGestureAnimationStart(mGestureState.getRunningTask());
     }
 
     private void launcherFrameDrawn() {
@@ -451,12 +444,6 @@
     @Override
     public void onMotionPauseChanged(boolean isPaused) {
         setShelfState(isPaused ? PEEK : HIDE, ShelfPeekAnim.INTERPOLATOR, ShelfPeekAnim.DURATION);
-
-        if (mDeviceState.isFullyGesturalNavMode() && isPaused) {
-            // In fully gestural nav mode, switch to overview predictions once the user has paused
-            // (this is a no-op if the predictions are already in that state)
-            mActivityInterface.updateOverviewPredictionState();
-        }
     }
 
     public void maybeUpdateRecentsAttachedState() {
@@ -555,14 +542,6 @@
 
     @Override
     public void updateFinalShift() {
-        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            if (mRecentsAnimationTargets != null) {
-                LiveTileOverlay.INSTANCE.update(
-                        mTaskViewSimulator.getCurrentCropRect(),
-                        mTaskViewSimulator.getCurrentCornerRadius());
-            }
-        }
-
         final boolean passed = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW;
         if (passed != mPassedOverviewThreshold) {
             mPassedOverviewThreshold = passed;
@@ -573,6 +552,14 @@
 
         updateSysUiFlags(mCurrentShift.value);
         applyWindowTransform();
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+            if (mRecentsAnimationTargets != null) {
+                LiveTileOverlay.INSTANCE.update(
+                        mTaskViewSimulator.getCurrentRect(),
+                        mTaskViewSimulator.getCurrentCornerRadius());
+            }
+        }
+
         updateLauncherTransitionProgress();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index edefbe1..6621b41 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -262,16 +262,6 @@
     }
 
     @Override
-    public void updateOverviewPredictionState() {
-        Launcher launcher = getCreatedActivity();
-        if (launcher == null) {
-            return;
-        }
-        PredictionUiStateManager.INSTANCE.get(launcher).switchClient(
-                PredictionUiStateManager.Client.OVERVIEW);
-    }
-
-    @Override
     public int getContainerType() {
         final Launcher launcher = getVisibleLauncher();
         return launcher != null ? launcher.getStateManager().getState().containerType
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
index 434a929..dca3378 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/OverviewCommandHelper.java
@@ -29,7 +29,6 @@
 
 import androidx.annotation.BinderThread;
 
-import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.statemanager.StatefulActivity;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -165,7 +164,7 @@
             mActivityInterface = mOverviewComponentObserver.getActivityInterface();
             mCreateTime = SystemClock.elapsedRealtime();
             mAnimationProvider = new AppToOverviewAnimationProvider<>(mActivityInterface,
-                    RecentsModel.getRunningTaskId(), mDeviceState);
+                    ActivityManagerWrapper.getInstance().getRunningTask(), mDeviceState);
 
             // Preload the plan
             mRecentsModel.getTasks(null);
@@ -227,10 +226,6 @@
                         LauncherLogProto.ContainerType.TASKSWITCHER);
                 mUserEventLogged = true;
             }
-
-            // Switch prediction client to overview
-            PredictionUiStateManager.INSTANCE.get(activity).switchClient(
-                    PredictionUiStateManager.Client.OVERVIEW);
             return mAnimationProvider.onActivityReady(activity, wasVisible);
         }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index d20bbe9..ffe9d6a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -76,7 +76,7 @@
      */
     public void onGestureAnimationStartOnHome(RunningTaskInfo homeTaskInfo) {
         mHomeTaskInfo = homeTaskInfo;
-        onGestureAnimationStart(homeTaskInfo == null ? -1 : homeTaskInfo.taskId);
+        onGestureAnimationStart(homeTaskInfo);
     }
 
     /**
@@ -107,14 +107,15 @@
     }
 
     @Override
-    protected boolean shouldAddDummyTaskView(int runningTaskId) {
-        if (mHomeTaskInfo != null && mHomeTaskInfo.taskId == runningTaskId
+    protected boolean shouldAddDummyTaskView(RunningTaskInfo runningTaskInfo) {
+        if (mHomeTaskInfo != null && runningTaskInfo != null &&
+                mHomeTaskInfo.taskId == runningTaskInfo.taskId
                 && getTaskViewCount() == 0) {
             // Do not add a dummy task if we are running over home with empty recents, so that we
             // show the empty recents message instead of showing a dummy task and later removing it.
             return false;
         }
-        return super.shouldAddDummyTaskView(runningTaskId);
+        return super.shouldAddDummyTaskView(runningTaskInfo);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
index 3cafd42..4120331 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/StaggeredWorkspaceAnim.java
@@ -205,13 +205,15 @@
         ResourceProvider rp = DynamicResource.provider(v.getContext());
         float stiffness = rp.getFloat(R.dimen.staggered_stiffness);
         float damping = rp.getFloat(R.dimen.staggered_damping_ratio);
+        float endTransY = 0;
+        float springVelocity = Math.abs(mVelocity) * Math.signum(endTransY - mSpringTransY);
         ValueAnimator springTransY = new SpringAnimationBuilder(v.getContext())
                 .setStiffness(stiffness)
                 .setDampingRatio(damping)
                 .setMinimumVisibleChange(1f)
                 .setStartValue(mSpringTransY)
-                .setEndValue(0)
-                .setStartVelocity(mVelocity)
+                .setEndValue(endTransY)
+                .setStartVelocity(springVelocity)
                 .build(v, VIEW_TRANSLATE_Y);
         springTransY.setStartDelay(startDelay);
         springTransY.addListener(new AnimatorListenerAdapter() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
index c9ed498..64ae1e0 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -15,6 +15,7 @@
  */
 package com.android.quickstep.util;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
 import static com.android.launcher3.states.RotationHelper.deltaRotation;
 import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
 import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
@@ -197,6 +198,15 @@
         return mTempRectF;
     }
 
+    /**
+     * Returns the current task bounds in the Launcher coordinate space.
+     */
+    public RectF getCurrentRect() {
+        RectF result = getCurrentCropRect();
+        mMatrix.mapRect(result);
+        return result;
+    }
+
     public RecentsOrientedState getOrientationState() {
         return mOrientationState;
     }
@@ -295,6 +305,10 @@
         builder.withMatrix(mMatrix)
                 .withWindowCrop(mTmpCropRect)
                 .withCornerRadius(getCurrentCornerRadius());
+
+        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) {
+            builder.withRelativeLayerTo(params.getRecentsSurface(), Integer.MAX_VALUE);
+        }
     }
 
     /**
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
index 0135f74..756331d 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TransformParams.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.util;
 
 import android.util.FloatProperty;
+import android.view.SurfaceControl;
 
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
@@ -58,6 +59,7 @@
     private float mCornerRadius;
     private RemoteAnimationTargets mTargetSet;
     private SurfaceTransactionApplier mSyncTransactionApplier;
+    private SurfaceControl mRecentsSurface;
 
     private BuilderProxy mHomeBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
     private BuilderProxy mBaseBuilderProxy = BuilderProxy.ALWAYS_VISIBLE;
@@ -138,6 +140,8 @@
     public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
         RemoteAnimationTargets targets = mTargetSet;
         SurfaceParams[] surfaceParams = new SurfaceParams[targets.unfilteredApps.length];
+        mRecentsSurface = getRecentsSurface(targets);
+
         for (int i = 0; i < targets.unfilteredApps.length; i++) {
             RemoteAnimationTargetCompat app = targets.unfilteredApps[i];
             SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash);
@@ -165,6 +169,20 @@
         return surfaceParams;
     }
 
+    private static SurfaceControl getRecentsSurface(RemoteAnimationTargets targets) {
+        for (int i = 0; i < targets.unfilteredApps.length; i++) {
+            RemoteAnimationTargetCompat app = targets.unfilteredApps[i];
+            if (app.mode == targets.targetMode) {
+                if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS) {
+                    return app.leash.getSurfaceControl();
+                }
+            } else {
+                return app.leash.getSurfaceControl();
+            }
+        }
+        return null;
+    }
+
     // Pubic getters so outside packages can read the values.
 
     public float getProgress() {
@@ -179,6 +197,10 @@
         return mCornerRadius;
     }
 
+    public SurfaceControl getRecentsSurface() {
+        return mRecentsSurface;
+    }
+
     public RemoteAnimationTargets getTargetSet() {
         return mTargetSet;
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
index 0979c07..3d44eb6 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/AllAppsEduView.java
@@ -16,6 +16,7 @@
 package com.android.quickstep.views;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
@@ -42,8 +43,10 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.Themes;
@@ -139,7 +142,12 @@
         config.userControlled = false;
         AnimatorPlaybackController stateAnimationController =
                 mLauncher.getStateManager().createAnimationToNewWorkspace(ALL_APPS, config);
-        float maxAllAppsProgress = 0.15f;
+        float maxAllAppsProgress = mLauncher.getDeviceProfile().isLandscape ? 0.35f : 0.15f;
+
+        AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
+        PendingAnimation allAppsAlpha = new PendingAnimation(config.duration);
+        allAppsController.setAlphas(ALL_APPS, config, allAppsAlpha);
+        mAnimation.play(allAppsAlpha.buildAnim());
 
         ValueAnimator intro = ValueAnimator.ofFloat(0, 1f);
         intro.setInterpolator(LINEAR);
@@ -191,7 +199,8 @@
             @Override
             public void onAnimationEnd(Animator animation) {
                 mAnimation = null;
-                stateAnimationController.dispatchOnCancel();
+                // Handles cancelling the animation used to hint towards All Apps.
+                mLauncher.getStateManager().goToState(NORMAL, false);
                 handleClose(false);
             }
         });
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 846b944..59c6815 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -41,8 +41,6 @@
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.appprediction.PredictionUiStateManager;
-import com.android.launcher3.appprediction.PredictionUiStateManager.Client;
 import com.android.launcher3.model.AppLaunchTracker;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager.StateListener;
@@ -237,9 +235,6 @@
         super.reset();
 
         setLayoutRotation(Surface.ROTATION_0, Surface.ROTATION_0);
-        // We are moving to home or some other UI with no recents. Switch back to the home client,
-        // the home predictions should have been updated when the activity was resumed.
-        PredictionUiStateManager.INSTANCE.get(getContext()).switchClient(Client.HOME);
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 7b24b03..9c25b24 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -56,6 +56,7 @@
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -135,6 +136,7 @@
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
 import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.LauncherEventUtil;
@@ -147,7 +149,7 @@
 /**
  * A list of recent tasks.
  */
-@TargetApi(Build.VERSION_CODES.P)
+@TargetApi(Build.VERSION_CODES.R)
 public abstract class RecentsView<T extends StatefulActivity> extends PagedView implements
         Insettable, TaskThumbnailCache.HighResLoadingState.HighResLoadingStateChangedCallback,
         InvariantDeviceProfile.OnIDPChangeListener, TaskVisualsChangeListener,
@@ -1041,13 +1043,13 @@
     /**
      * Called when a gesture from an app is starting.
      */
-    public void onGestureAnimationStart(int runningTaskId) {
+    public void onGestureAnimationStart(RunningTaskInfo runningTaskInfo) {
         // This needs to be called before the other states are set since it can create the task view
         if (mOrientationState.setGestureActive(true)) {
             updateOrientationHandler();
         }
 
-        showCurrentTask(runningTaskId);
+        showCurrentTask(runningTaskInfo);
         setEnableFreeScroll(false);
         setEnableDrawingLiveTile(false);
         setRunningTaskHidden(true);
@@ -1127,8 +1129,8 @@
     /**
      * Returns true if we should add a dummy taskView for the running task id
      */
-    protected boolean shouldAddDummyTaskView(int runningTaskId) {
-        return getTaskView(runningTaskId) == null;
+    protected boolean shouldAddDummyTaskView(RunningTaskInfo runningTaskInfo) {
+        return runningTaskInfo != null && getTaskView(runningTaskInfo.taskId) == null;
     }
 
     /**
@@ -1137,8 +1139,8 @@
      * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
      * is called.  Also scrolls the view to this task.
      */
-    public void showCurrentTask(int runningTaskId) {
-        if (shouldAddDummyTaskView(runningTaskId)) {
+    public void showCurrentTask(RunningTaskInfo runningTaskInfo) {
+        if (shouldAddDummyTaskView(runningTaskInfo)) {
             boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
@@ -1148,10 +1150,7 @@
             }
             // The temporary running task is only used for the duration between the start of the
             // gesture and the task list is loaded and applied
-            mTmpRunningTask = new Task(new Task.TaskKey(runningTaskId, 0, new Intent(),
-                    new ComponentName(getContext(), getClass()), 0, 0), null, null, "", "", 0, 0,
-                    false, true, false, false, new ActivityManager.TaskDescription(), 0,
-                    new ComponentName("", ""), false);
+            mTmpRunningTask = Task.from(new TaskKey(runningTaskInfo), runningTaskInfo, false);
             taskView.bind(mTmpRunningTask, mOrientationState);
 
             // Measure and layout immediately so that the scroll values is updated instantly
@@ -1162,7 +1161,7 @@
         }
 
         boolean runningTaskTileHidden = mRunningTaskTileHidden;
-        setCurrentTask(runningTaskId);
+        setCurrentTask(runningTaskInfo == null ? -1 : runningTaskInfo.taskId);
         setCurrentPage(getRunningTaskIndex());
         setRunningTaskViewShowScreenshot(false);
         setRunningTaskHidden(runningTaskTileHidden);
@@ -1680,7 +1679,7 @@
                 : View.LAYOUT_DIRECTION_RTL);
         mClearAllButton.setRotation(mOrientationHandler.getDegreesRotated());
         mActivity.getDragLayer().recreateControllers();
-        boolean isInLandscape = mOrientationState.getTouchRotation() != 0
+        boolean isInLandscape = mOrientationState.getTouchRotation() != ROTATION_0
                 || mOrientationState.getRecentsActivityRotation() != ROTATION_0;
         mActionsView.updateHiddenFlags(HIDDEN_NON_ZERO_ROTATION,
                 !mOrientationState.canRecentsActivityRotate() && isInLandscape);
diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
index 93b64e6..c148a4b 100644
--- a/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
+++ b/quickstep/robolectric_tests/src/com/android/quickstep/RecentsActivityTest.java
@@ -17,6 +17,7 @@
 
 import static com.android.launcher3.util.LauncherUIHelper.doLayout;
 
+import android.app.ActivityManager.RunningTaskInfo;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 
@@ -50,7 +51,7 @@
     }
 
     @Test
-    public void testRecets_showCurrentTask() {
+    public void testRecents_showCurrentTask() {
         ActivityController<RecentsActivity> controller =
                 Robolectric.buildActivity(RecentsActivity.class);
 
@@ -58,7 +59,10 @@
         doLayout(activity);
 
         FallbackRecentsView frv = activity.getOverviewPanel();
-        frv.showCurrentTask(22);
+
+        RunningTaskInfo dummyTask = new RunningTaskInfo();
+        dummyTask.taskId = 22;
+        frv.showCurrentTask(dummyTask);
         doLayout(activity);
 
         ThumbnailData thumbnailData = new ThumbnailData();
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 47ce320..6b941be 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -157,6 +157,12 @@
     @Override
     protected void onDeferredResumed() {
         super.onDeferredResumed();
+        handlePendingActivityRequest();
+    }
+
+    @Override
+    protected void handlePendingActivityRequest() {
+        super.handlePendingActivityRequest();
         if (mPendingActivityRequestCode != -1 && isInState(NORMAL)) {
             // Remove any active ProxyActivityStarter task and send RESULT_CANCELED to Launcher.
             onActivityResult(mPendingActivityRequestCode, RESULT_CANCELED, null);
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 10f789d..2d096d1 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.launcher3;
 
-import static android.util.TypedValue.COMPLEX_UNIT_DIP;
-
 import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
@@ -62,7 +60,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Pair;
-import android.util.TypedValue;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -879,10 +876,8 @@
                             }
                         });
                     } else {
-                        float velocityDpPerS = DynamicResource.provider(mLauncher)
+                        float velocityPxPerS = DynamicResource.provider(mLauncher)
                                 .getDimension(R.dimen.unlock_staggered_velocity_dp_per_s);
-                        float velocityPxPerS = TypedValue.applyDimension(COMPLEX_UNIT_DIP,
-                                velocityDpPerS, mLauncher.getResources().getDisplayMetrics());
                         anim.play(new StaggeredWorkspaceAnim(mLauncher, velocityPxPerS, false)
                                 .getAnimators());
                     }
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 2b698bd..bdeb3a6 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -154,13 +154,6 @@
             Runnable exitRunnable);
 
     /**
-     * Updates the prediction state to the overview state.
-     */
-    public void updateOverviewPredictionState() {
-        // By public overview predictions are not supported
-    }
-
-    /**
      * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
      */
     public abstract int getContainerType();
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 517501a..6f54ba2 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -93,15 +93,6 @@
     }
 
     /**
-     * @return The task id of the running task, or -1 if there is no current running task.
-     */
-    public static int getRunningTaskId() {
-        ActivityManager.RunningTaskInfo runningTask =
-                ActivityManagerWrapper.getInstance().getRunningTask();
-        return runningTask != null ? runningTask.id : -1;
-    }
-
-    /**
      * @return Whether the provided {@param changeId} is the latest recent tasks list id.
      */
     public boolean isTaskListValid(int changeId) {
@@ -140,7 +131,9 @@
         }
 
         // Keep the cache up to date with the latest thumbnails
-        int runningTaskId = RecentsModel.getRunningTaskId();
+        ActivityManager.RunningTaskInfo runningTask =
+                ActivityManagerWrapper.getInstance().getRunningTask();
+        int runningTaskId = runningTask != null ? runningTask.id : -1;
         mTaskList.getTaskKeys(mThumbnailCache.getCacheSize(), tasks -> {
             for (Task task : tasks) {
                 if (task.key.id == runningTaskId) {
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index ae19d73..f7bd1e2 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -49,7 +49,7 @@
             Rect taskSize = new Rect();
             LauncherActivityInterface.INSTANCE.calculateTaskSize(context, dp, taskSize,
                     orientationHandler);
-            return (dp.heightPx - taskSize.height()) / 2;
+            return orientationHandler.getDistanceToBottomOfRect(dp, taskSize);
         }
         int shelfHeight = dp.hotseatBarSizePx + dp.getInsets().bottom;
         int spaceBetweenShelfAndRecents = (int) context.getResources().getDimension(
diff --git a/res/layout/launcher_preview_layout.xml b/res/layout/launcher_preview_layout.xml
index 3fd02e3..4a20c70 100644
--- a/res/layout/launcher_preview_layout.xml
+++ b/res/layout/launcher_preview_layout.xml
@@ -17,7 +17,8 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:launcher="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:focusable="false">
 
     <com.android.launcher3.CellLayout
         android:id="@+id/workspace"
diff --git a/res/values/config.xml b/res/values/config.xml
index ca25325..75fcc90 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -142,7 +142,7 @@
 
     <item name="staggered_damping_ratio" type="dimen" format="float">0.7</item>
     <item name="staggered_stiffness" type="dimen" format="float">150</item>
-    <dimen name="unlock_staggered_velocity_dp_per_s">3dp</dimen>
+    <dimen name="unlock_staggered_velocity_dp_per_s">4dp</dimen>
 
     <item name="hint_scale_damping_ratio" type="dimen" format="float">0.7</item>
     <item name="hint_scale_stiffness" type="dimen" format="float">200</item>
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 8eceec0..41eeb78 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -183,6 +183,10 @@
     public void onScrollStateChanged(int state) {
         super.onScrollStateChanged(state);
 
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onScrollStateChanged: " + state);
+        }
+
         if (state == SCROLL_STATE_IDLE) {
             AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
         }
@@ -192,6 +196,10 @@
     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
         super.onInitializeAccessibilityNodeInfo(info);
         if (isLayoutSuppressed()) info.setScrollable(false);
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
+                    "onInitializeAccessibilityNodeInfo, scrollable: " + info.isScrollable());
+        }
     }
 
     @Override
@@ -199,8 +207,12 @@
         final boolean changing = frozen != isLayoutSuppressed();
         super.setLayoutFrozen(frozen);
         if (changing) {
-            ActivityContext.lookupContext(getContext()).getDragLayer()
-                    .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
+            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "setLayoutFrozen " + frozen
+                        + " @ " + Log.getStackTraceString(new Throwable()));
+                ActivityContext.lookupContext(getContext()).getDragLayer()
+                        .sendAccessibilityEvent(TYPE_WINDOW_CONTENT_CHANGED);
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0970dae..d06ae7a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -923,6 +923,7 @@
         DiscoveryBounce.showForHomeIfNeeded(this);
     }
 
+    protected void handlePendingActivityRequest() { }
 
     private void logStopAndResume(int command) {
         int pageIndex = mWorkspace.isOverlayShown() ? -1 : mWorkspace.getCurrentPage();
@@ -1423,7 +1424,8 @@
                 if (!isInState(NORMAL)) {
                     // Only change state, if not already the same. This prevents cancelling any
                     // animations running as part of resume
-                    mStateManager.goToState(NORMAL);
+                    mStateManager.goToState(NORMAL, mStateManager.shouldAnimateStateChange(),
+                            this::handlePendingActivityRequest);
                 }
 
                 // Reset the apps view
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index f434c91..ff4b545 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -58,7 +58,7 @@
 import com.android.launcher3.pm.PackageInstallInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutRequest;
-import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.PackageUserKey;
@@ -410,7 +410,7 @@
         enqueueModelUpdateTask(new BaseModelUpdateTask() {
             @Override
             public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
-                final IntSparseArrayMap<Boolean> removedIds = new IntSparseArrayMap<>();
+                final IntSet removedIds = new IntSet();
                 synchronized (dataModel) {
                     for (ItemInfo info : dataModel.itemsIdMap) {
                         if (info instanceof WorkspaceItemInfo
@@ -418,13 +418,13 @@
                                 && user.equals(info.user)
                                 && info.getIntent() != null
                                 && TextUtils.equals(packageName, info.getIntent().getPackage())) {
-                            removedIds.put(info.id, true /* remove */);
+                            removedIds.add(info.id);
                         }
                     }
                 }
 
                 if (!removedIds.isEmpty()) {
-                    deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds, false));
+                    deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds));
                 }
             }
         });
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index c78df62..39b0f2f 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -250,7 +250,8 @@
     }
 
     public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
-        if (this != NORMAL || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
+        if ((this != NORMAL && this != HINT_STATE)
+                || !launcher.getDeviceProfile().shouldFadeAdjacentWorkspaceScreens()) {
             return DEFAULT_ALPHA_PROVIDER;
         }
         final int centerPage = launcher.getWorkspace().getNextPage();
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index c989e7b..77b8a32 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -83,7 +83,7 @@
     protected final BaseDraggingActivity mLauncher;
     protected final AdapterHolder[] mAH;
     private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle());
-    private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher);
+    private final ItemInfoMatcher mWorkMatcher = mPersonalMatcher.negate();
     private final AllAppsStore mAllAppsStore = new AllAppsStore();
 
     private final Paint mNavBarScrimPaint;
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 1d32d1d..d86bb17 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -75,6 +75,9 @@
     }
 
     public static void sendScrollFinishedEventToTest(Context context) {
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "sendScrollFinishedEventToTest");
+        }
         final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
         if (accessibilityManager == null) return;
 
@@ -94,6 +97,9 @@
                 AccessibilityEvent.TYPE_ANNOUNCEMENT);
         e.setClassName(eventTag);
         e.setParcelableData(data);
+        if (TestProtocol.sDebugTracing) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "sendEventToTest " + e);
+        }
         accessibilityManager.sendAccessibilityEvent(e);
     }
 
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index d01e189..1c18402 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -241,9 +241,9 @@
         mFolderName.setSelectAllOnFocus(true);
         mFolderName.setInputType(mFolderName.getInputType()
                 & ~InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
-                & ~InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
+                | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
-        mFolderName.forceDisableSuggestions(!FeatureFlags.FOLDER_NAME_SUGGEST.get());
+        mFolderName.forceDisableSuggestions(true);
 
         mFooter = findViewById(R.id.folder_footer);
 
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 9bef847..7524920 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -56,6 +56,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 /**
@@ -348,6 +349,19 @@
         }
     }
 
+    /**
+     * Calls the provided {@code op} for all workspaceItems in the in-memory model (both persisted
+     * items and dynamic/predicted items for the provided {@code userHandle}.
+     * Note the call is not synchronized over the model, that should be handled by the called.
+     */
+    public void forAllWorkspaceItemInfos(UserHandle userHandle, Consumer<WorkspaceItemInfo> op) {
+        for (ItemInfo info : itemsIdMap) {
+            if (info instanceof WorkspaceItemInfo && userHandle.equals(info.user)) {
+                op.accept((WorkspaceItemInfo) info);
+            }
+        }
+    }
+
     public interface Callbacks {
         // If the launcher has permission to access deep shortcuts.
         int FLAG_HAS_SHORTCUT_PERMISSION = 1 << 0;
diff --git a/src/com/android/launcher3/model/CacheDataUpdatedTask.java b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
index 8e6b064..f644d49 100644
--- a/src/com/android/launcher3/model/CacheDataUpdatedTask.java
+++ b/src/com/android/launcher3/model/CacheDataUpdatedTask.java
@@ -21,7 +21,6 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 
 import java.util.ArrayList;
@@ -48,23 +47,18 @@
     @Override
     public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) {
         IconCache iconCache = app.getIconCache();
-
-
         ArrayList<WorkspaceItemInfo> updatedShortcuts = new ArrayList<>();
 
         synchronized (dataModel) {
-            for (ItemInfo info : dataModel.itemsIdMap) {
-                if (info instanceof WorkspaceItemInfo && mUser.equals(info.user)) {
-                    WorkspaceItemInfo si = (WorkspaceItemInfo) info;
-                    ComponentName cn = si.getTargetComponent();
-                    if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
-                            && isValidShortcut(si) && cn != null
-                            && mPackages.contains(cn.getPackageName())) {
-                        iconCache.getTitleAndIcon(si, si.usingLowResIcon());
-                        updatedShortcuts.add(si);
-                    }
+            dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+                ComponentName cn = si.getTargetComponent();
+                if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                        && isValidShortcut(si) && cn != null
+                        && mPackages.contains(cn.getPackageName())) {
+                    iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                    updatedShortcuts.add(si);
                 }
-            }
+            });
             apps.updateIconsAndLabels(mPackages, mUser);
         }
         bindUpdatedWorkspaceItems(updatedShortcuts);
diff --git a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
index 203f1ca..8369c48 100644
--- a/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
+++ b/src/com/android/launcher3/model/PackageInstallStateChangedTask.java
@@ -20,8 +20,6 @@
 import android.content.pm.PackageManager;
 
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
 import com.android.launcher3.model.data.PromiseAppInfo;
@@ -70,21 +68,18 @@
 
         synchronized (dataModel) {
             final HashSet<ItemInfo> updates = new HashSet<>();
-            for (ItemInfo info : dataModel.itemsIdMap) {
-                if (info instanceof WorkspaceItemInfo) {
-                    WorkspaceItemInfo si = (WorkspaceItemInfo) info;
-                    ComponentName cn = si.getTargetComponent();
-                    if (si.hasPromiseIconUi() && (cn != null)
-                            && mInstallInfo.packageName.equals(cn.getPackageName())) {
-                        si.setInstallProgress(mInstallInfo.progress);
-                        if (mInstallInfo.state == PackageInstallInfo.STATUS_FAILED) {
-                            // Mark this info as broken.
-                            si.status &= ~WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE;
-                        }
-                        updates.add(si);
+            dataModel.forAllWorkspaceItemInfos(mInstallInfo.user, si -> {
+                ComponentName cn = si.getTargetComponent();
+                if (si.hasPromiseIconUi() && (cn != null)
+                        && mInstallInfo.packageName.equals(cn.getPackageName())) {
+                    si.setInstallProgress(mInstallInfo.progress);
+                    if (mInstallInfo.state == PackageInstallInfo.STATUS_FAILED) {
+                        // Mark this info as broken.
+                        si.status &= ~WorkspaceItemInfo.FLAG_INSTALL_SESSION_ACTIVE;
                     }
+                    updates.add(si);
                 }
-            }
+            });
 
             for (LauncherAppWidgetInfo widget : dataModel.appWidgets) {
                 if (widget.providerName.getPackageName().equals(mInstallInfo.packageName)) {
@@ -94,12 +89,7 @@
             }
 
             if (!updates.isEmpty()) {
-                scheduleCallbackTask(new CallbackTask() {
-                    @Override
-                    public void execute(Callbacks callbacks) {
-                        callbacks.bindRestoreItemsChange(updates);
-                    }
-                });
+                scheduleCallbackTask(callbacks -> callbacks.bindRestoreItemsChange(updates));
             }
         }
     }
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 7cd467e..dca4ec0 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -45,7 +45,7 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.util.FlagOp;
-import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.PackageUserKey;
@@ -92,9 +92,11 @@
 
         final String[] packages = mPackages;
         final int N = packages.length;
-        FlagOp flagOp = FlagOp.NO_OP;
+        final FlagOp flagOp;
         final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
-        ItemInfoMatcher matcher = ItemInfoMatcher.ofPackages(packageSet, mUser);
+        final ItemInfoMatcher matcher = mOp == OP_USER_AVAILABILITY_CHANGE
+                ? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user
+                : ItemInfoMatcher.ofPackages(packageSet, mUser);
         final HashSet<ComponentName> removedComponents = new HashSet<>();
 
         switch (mOp) {
@@ -158,19 +160,22 @@
                 flagOp = ums.isUserQuiet(mUser)
                         ? FlagOp.addFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER)
                         : FlagOp.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER);
-                // We want to update all packages for this user.
-                matcher = ItemInfoMatcher.ofUser(mUser);
                 appsList.updateDisabledFlags(matcher, flagOp);
 
                 // We are not synchronizing here, as int operations are atomic
                 appsList.setFlags(FLAG_QUIET_MODE_ENABLED, ums.isAnyProfileQuietModeEnabled());
                 break;
             }
+            default:
+                flagOp = FlagOp.NO_OP;
+                break;
         }
 
         bindApplicationsIfNeeded();
 
-        final IntSparseArrayMap<Boolean> removedShortcuts = new IntSparseArrayMap<>();
+        final IntSet removedShortcuts = new IntSet();
+        // Shortcuts to keep even if the corresponding app was removed
+        final IntSet forceKeepShortcuts = new IntSet();
 
         // Update shortcut infos
         if (mOp == OP_ADD || flagOp != FlagOp.NO_OP) {
@@ -180,118 +185,118 @@
             // For system apps, package manager send OP_UPDATE when an app is enabled.
             final boolean isNewApkAvailable = mOp == OP_ADD || mOp == OP_UPDATE;
             synchronized (dataModel) {
-                for (ItemInfo info : dataModel.itemsIdMap) {
-                    if (info instanceof WorkspaceItemInfo && mUser.equals(info.user)) {
-                        WorkspaceItemInfo si = (WorkspaceItemInfo) info;
-                        boolean infoUpdated = false;
-                        boolean shortcutUpdated = false;
+                dataModel.forAllWorkspaceItemInfos(mUser, si -> {
 
-                        // Update shortcuts which use iconResource.
-                        if ((si.iconResource != null)
-                                && packageSet.contains(si.iconResource.packageName)) {
-                            LauncherIcons li = LauncherIcons.obtain(context);
-                            BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
-                            li.recycle();
-                            if (iconInfo != null) {
-                                si.bitmap = iconInfo;
-                                infoUpdated = true;
+                    boolean infoUpdated = false;
+                    boolean shortcutUpdated = false;
+
+                    // Update shortcuts which use iconResource.
+                    if ((si.iconResource != null)
+                            && packageSet.contains(si.iconResource.packageName)) {
+                        LauncherIcons li = LauncherIcons.obtain(context);
+                        BitmapInfo iconInfo = li.createIconBitmap(si.iconResource);
+                        li.recycle();
+                        if (iconInfo != null) {
+                            si.bitmap = iconInfo;
+                            infoUpdated = true;
+                        }
+                    }
+
+                    ComponentName cn = si.getTargetComponent();
+                    if (cn != null && matcher.matches(si, cn)) {
+                        String packageName = cn.getPackageName();
+
+                        if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
+                            forceKeepShortcuts.add(si.id);
+                            if (mOp == OP_REMOVE) {
+                                return;
                             }
                         }
 
-                        ComponentName cn = si.getTargetComponent();
-                        if (cn != null && matcher.matches(si, cn)) {
-                            String packageName = cn.getPackageName();
-
-                            if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)) {
-                                removedShortcuts.put(si.id, false);
-                                if (mOp == OP_REMOVE) {
-                                    continue;
-                                }
-                            }
-
-                            if (si.isPromise() && isNewApkAvailable) {
-                                boolean isTargetValid = true;
-                                if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                                    List<ShortcutInfo> shortcut =
-                                            new ShortcutRequest(context, mUser)
-                                                    .forPackage(cn.getPackageName(),
-                                                            si.getDeepShortcutId())
-                                                    .query(ShortcutRequest.PINNED);
-                                    if (shortcut.isEmpty()) {
-                                        isTargetValid = false;
-                                    } else {
-                                        si.updateFromDeepShortcutInfo(shortcut.get(0), context);
-                                        infoUpdated = true;
-                                    }
-                                } else if (!cn.getClassName().equals(IconCache.EMPTY_CLASS_NAME)) {
-                                    isTargetValid = context.getSystemService(LauncherApps.class)
-                                            .isActivityEnabled(cn, mUser);
-                                }
-                                if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
-                                    if (updateWorkspaceItemIntent(context, si, packageName)) {
-                                        infoUpdated = true;
-                                    } else if (si.hasPromiseIconUi()) {
-                                        removedShortcuts.put(si.id, true);
-                                        continue;
-                                    }
-                                } else if (!isTargetValid) {
-                                    removedShortcuts.put(si.id, true);
-                                    FileLog.e(TAG, "Restored shortcut no longer valid "
-                                            + si.getIntent());
-                                    continue;
+                        if (si.isPromise() && isNewApkAvailable) {
+                            boolean isTargetValid = true;
+                            if (si.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                                List<ShortcutInfo> shortcut =
+                                        new ShortcutRequest(context, mUser)
+                                                .forPackage(cn.getPackageName(),
+                                                        si.getDeepShortcutId())
+                                                .query(ShortcutRequest.PINNED);
+                                if (shortcut.isEmpty()) {
+                                    isTargetValid = false;
                                 } else {
-                                    si.status = WorkspaceItemInfo.DEFAULT;
+                                    si.updateFromDeepShortcutInfo(shortcut.get(0), context);
                                     infoUpdated = true;
                                 }
-                            } else if (isNewApkAvailable && removedComponents.contains(cn)) {
+                            } else if (!cn.getClassName().equals(IconCache.EMPTY_CLASS_NAME)) {
+                                isTargetValid = context.getSystemService(LauncherApps.class)
+                                        .isActivityEnabled(cn, mUser);
+                            }
+                            if (si.hasStatusFlag(FLAG_RESTORED_ICON | FLAG_AUTOINSTALL_ICON)) {
                                 if (updateWorkspaceItemIntent(context, si, packageName)) {
                                     infoUpdated = true;
+                                } else if (si.hasPromiseIconUi()) {
+                                    removedShortcuts.add(si.id);
+                                    return;
                                 }
-                            }
-
-                            if (isNewApkAvailable &&
-                                    si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
-                                iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                            } else if (!isTargetValid) {
+                                removedShortcuts.add(si.id);
+                                FileLog.e(TAG, "Restored shortcut no longer valid "
+                                        + si.getIntent());
+                                return;
+                            } else {
+                                si.status = WorkspaceItemInfo.DEFAULT;
                                 infoUpdated = true;
                             }
-
-                            int oldRuntimeFlags = si.runtimeStatusFlags;
-                            si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags);
-                            if (si.runtimeStatusFlags != oldRuntimeFlags) {
-                                shortcutUpdated = true;
+                        } else if (isNewApkAvailable && removedComponents.contains(cn)) {
+                            if (updateWorkspaceItemIntent(context, si, packageName)) {
+                                infoUpdated = true;
                             }
                         }
 
-                        if (infoUpdated || shortcutUpdated) {
-                            updatedWorkspaceItems.add(si);
+                        if (isNewApkAvailable
+                                && si.itemType == Favorites.ITEM_TYPE_APPLICATION) {
+                            iconCache.getTitleAndIcon(si, si.usingLowResIcon());
+                            infoUpdated = true;
                         }
-                        if (infoUpdated) {
-                            getModelWriter().updateItemInDatabase(si);
-                        }
-                    } else if (info instanceof LauncherAppWidgetInfo && isNewApkAvailable) {
-                        LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo) info;
-                        if (mUser.equals(widgetInfo.user)
-                                && widgetInfo.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
-                                && packageSet.contains(widgetInfo.providerName.getPackageName())) {
-                            widgetInfo.restoreStatus &=
-                                    ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY &
-                                            ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
 
-                            // adding this flag ensures that launcher shows 'click to setup'
-                            // if the widget has a config activity. In case there is no config
-                            // activity, it will be marked as 'restored' during bind.
-                            widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
-
-                            widgets.add(widgetInfo);
-                            getModelWriter().updateItemInDatabase(widgetInfo);
+                        int oldRuntimeFlags = si.runtimeStatusFlags;
+                        si.runtimeStatusFlags = flagOp.apply(si.runtimeStatusFlags);
+                        if (si.runtimeStatusFlags != oldRuntimeFlags) {
+                            shortcutUpdated = true;
                         }
                     }
+
+                    if (infoUpdated || shortcutUpdated) {
+                        updatedWorkspaceItems.add(si);
+                    }
+                    if (infoUpdated && si.id != ItemInfo.NO_ID) {
+                        getModelWriter().updateItemInDatabase(si);
+                    }
+                });
+
+                for (LauncherAppWidgetInfo widgetInfo : dataModel.appWidgets) {
+                    if (mUser.equals(widgetInfo.user)
+                            && widgetInfo.hasRestoreFlag(
+                                    LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY)
+                            && packageSet.contains(widgetInfo.providerName.getPackageName())) {
+                        widgetInfo.restoreStatus &=
+                                ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
+                                        & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
+
+                        // adding this flag ensures that launcher shows 'click to setup'
+                        // if the widget has a config activity. In case there is no config
+                        // activity, it will be marked as 'restored' during bind.
+                        widgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
+
+                        widgets.add(widgetInfo);
+                        getModelWriter().updateItemInDatabase(widgetInfo);
+                    }
                 }
             }
 
             bindUpdatedWorkspaceItems(updatedWorkspaceItems);
             if (!removedShortcuts.isEmpty()) {
-                deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts, false));
+                deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedShortcuts));
             }
 
             if (!widgets.isEmpty()) {
@@ -319,7 +324,7 @@
         if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
             ItemInfoMatcher removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser)
                     .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
-                    .and(ItemInfoMatcher.ofItemIds(removedShortcuts, true));
+                    .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
             deleteAndBindComponentsRemoved(removeMatch);
 
             // Remove any queued items from the install queue
diff --git a/src/com/android/launcher3/model/ShortcutsChangedTask.java b/src/com/android/launcher3/model/ShortcutsChangedTask.java
index 1cbe5c2..88006ba 100644
--- a/src/com/android/launcher3/model/ShortcutsChangedTask.java
+++ b/src/com/android/launcher3/model/ShortcutsChangedTask.java
@@ -21,7 +21,6 @@
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
@@ -58,14 +57,14 @@
         MultiHashMap<ShortcutKey, WorkspaceItemInfo> keyToShortcutInfo = new MultiHashMap<>();
         HashSet<String> allIds = new HashSet<>();
 
-        for (ItemInfo itemInfo : dataModel.itemsIdMap) {
-            if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
-                WorkspaceItemInfo si = (WorkspaceItemInfo) itemInfo;
-                if (mPackageName.equals(si.getIntent().getPackage()) && si.user.equals(mUser)) {
+        synchronized (dataModel) {
+            dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+                if ((si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                        && mPackageName.equals(si.getIntent().getPackage())) {
                     keyToShortcutInfo.addToList(ShortcutKey.fromItemInfo(si), si);
                     allIds.add(si.getDeepShortcutId());
                 }
-            }
+            });
         }
 
         final ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
diff --git a/src/com/android/launcher3/model/UserLockStateChangedTask.java b/src/com/android/launcher3/model/UserLockStateChangedTask.java
index 7ec884f..5048e13 100644
--- a/src/com/android/launcher3/model/UserLockStateChangedTask.java
+++ b/src/com/android/launcher3/model/UserLockStateChangedTask.java
@@ -23,7 +23,6 @@
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.shortcuts.ShortcutRequest;
@@ -73,27 +72,27 @@
         ArrayList<WorkspaceItemInfo> updatedWorkspaceItemInfos = new ArrayList<>();
         HashSet<ShortcutKey> removedKeys = new HashSet<>();
 
-        for (ItemInfo itemInfo : dataModel.itemsIdMap) {
-            if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                    && mUser.equals(itemInfo.user)) {
-                WorkspaceItemInfo si = (WorkspaceItemInfo) itemInfo;
-                if (mIsUserUnlocked) {
-                    ShortcutKey key = ShortcutKey.fromItemInfo(si);
-                    ShortcutInfo shortcut = pinnedShortcuts.get(key);
-                    // We couldn't verify the shortcut during loader. If its no longer available
-                    // (probably due to clear data), delete the workspace item as well
-                    if (shortcut == null) {
-                        removedKeys.add(key);
-                        continue;
+        synchronized (dataModel) {
+            dataModel.forAllWorkspaceItemInfos(mUser, si -> {
+                if (si.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                    if (mIsUserUnlocked) {
+                        ShortcutKey key = ShortcutKey.fromItemInfo(si);
+                        ShortcutInfo shortcut = pinnedShortcuts.get(key);
+                        // We couldn't verify the shortcut during loader. If its no longer available
+                        // (probably due to clear data), delete the workspace item as well
+                        if (shortcut == null) {
+                            removedKeys.add(key);
+                            return;
+                        }
+                        si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER;
+                        si.updateFromDeepShortcutInfo(shortcut, context);
+                        app.getIconCache().getShortcutIcon(si, shortcut);
+                    } else {
+                        si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
                     }
-                    si.runtimeStatusFlags &= ~FLAG_DISABLED_LOCKED_USER;
-                    si.updateFromDeepShortcutInfo(shortcut, context);
-                    app.getIconCache().getShortcutIcon(si, shortcut);
-                } else {
-                    si.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
+                    updatedWorkspaceItemInfos.add(si);
                 }
-                updatedWorkspaceItemInfos.add(si);
-            }
+            });
         }
         bindUpdatedWorkspaceItems(updatedWorkspaceItemInfos);
         if (!removedKeys.isEmpty()) {
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 38b3712..d4a132e 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.util.ResourceBasedOverride;
+import com.android.launcher3.widget.WidgetsFullSheet;
 
 import java.util.concurrent.ExecutionException;
 import java.util.function.Function;
@@ -92,6 +93,11 @@
                         l -> l.getAppsView().getActiveRecyclerView().getCurrentScrollY());
             }
 
+            case TestProtocol.REQUEST_WIDGETS_SCROLL_Y: {
+                return getLauncherUIProperty(Bundle::putInt,
+                        l -> WidgetsFullSheet.getWidgetsView(l).getCurrentScrollY());
+            }
+
             case TestProtocol.REQUEST_WINDOW_INSETS: {
                 return getUIProperty(Bundle::putParcelable, a -> {
                     WindowInsets insets = a.getWindow()
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index 3ca08fd..2644db8 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -81,6 +81,7 @@
     public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list";
     public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags";
     public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
+    public static final String REQUEST_WIDGETS_SCROLL_Y = "widgets-scroll-y";
     public static final String REQUEST_WINDOW_INSETS = "window-insets";
     public static final String REQUEST_PID = "pid";
     public static final String REQUEST_TOTAL_PSS_KB = "total_pss";
@@ -106,4 +107,5 @@
     public static final String PAUSE_NOT_DETECTED = "b/139891609";
     public static final String OVERIEW_NOT_ALLAPPS = "b/156095088";
     public static final String NO_SWIPE_TO_HOME = "b/158017601";
+    public static final String NO_SCROLL_END_WIDGETS = "b/160238801";
 }
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index 48c7734..c2bfb16 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.touch;
 
 import static android.widget.ListPopupWindow.WRAP_CONTENT;
+
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.HORIZONTAL;
@@ -33,6 +34,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -260,4 +262,10 @@
         }
         return new ChildBounds(childHeight, childWidth, childBottom, childLeft);
     }
+
+    @SuppressWarnings("SuspiciousNameCombination")
+    @Override
+    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+        return rect.left;
+    }
 }
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index 65b1a7a..b650526 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -96,6 +96,7 @@
     int getTaskMenuWidth(View view);
     int getTaskMenuLayoutOrientation(LinearLayout taskMenuLayout);
     void setLayoutParamsForTaskMenuOptionItem(LinearLayout.LayoutParams lp);
+    int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect);
 
     /**
      * Maps the velocity from the coordinate plane of the foreground app to that
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 79e5c87..e87c887 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -32,6 +32,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.LinearLayout;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -257,4 +258,9 @@
         }
         return new ChildBounds(childWidth, childHeight, childRight, childTop);
     }
+
+    @Override
+    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+        return dp.heightPx - rect.bottom;
+    }
 }
diff --git a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
index d5ae2dc..e91f16d 100644
--- a/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/SeascapePagedViewHandler.java
@@ -18,9 +18,11 @@
 
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.view.Surface;
 import android.view.View;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
 
 public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
@@ -77,4 +79,9 @@
         view.setTranslationX(0);
         view.setTranslationY(translation);
     }
+
+    @Override
+    public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
+        return dp.widthPx - rect.right;
+    }
 }
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index 4d5405d..e98af35 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -81,11 +81,10 @@
     }
 
     /**
-     * Returns a new matcher which returns the opposite boolean value of the provided
-     * {@param matcher}.
+     * Returns a new matcher with returns the opposite value of this.
      */
-    static ItemInfoMatcher not(ItemInfoMatcher matcher) {
-        return (info, cn) -> !matcher.matches(info, cn);
+    default ItemInfoMatcher negate() {
+        return (info, cn) -> !matches(info, cn);
     }
 
     static ItemInfoMatcher ofUser(UserHandle user) {
@@ -105,7 +104,10 @@
                         keys.contains(ShortcutKey.fromItemInfo(info));
     }
 
-    static ItemInfoMatcher ofItemIds(IntSparseArrayMap<Boolean> ids, Boolean matchDefault) {
-        return (info, cn) -> ids.get(info.id, matchDefault);
+    /**
+     * Returns a matcher for items with provided ids
+     */
+    static ItemInfoMatcher ofItemIds(IntSet ids) {
+        return (info, cn) -> ids.contains(info.id);
     }
 }
diff --git a/src/com/android/launcher3/util/OnboardingPrefs.java b/src/com/android/launcher3/util/OnboardingPrefs.java
index d4e074c..26313e5 100644
--- a/src/com/android/launcher3/util/OnboardingPrefs.java
+++ b/src/com/android/launcher3/util/OnboardingPrefs.java
@@ -64,7 +64,7 @@
 
     private static final Map<String, Integer> MAX_COUNTS;
     static {
-        Map<String, Integer> maxCounts = new ArrayMap<>(3);
+        Map<String, Integer> maxCounts = new ArrayMap<>(4);
         maxCounts.put(HOME_BOUNCE_COUNT, 3);
         maxCounts.put(SHELF_BOUNCE_COUNT, 3);
         maxCounts.put(ALL_APPS_COUNT, 5);
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index b010b4b..2c75c74 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -292,6 +292,9 @@
 
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "BaseDragLayer: " + ev);
+        }
         switch (ev.getAction()) {
             case ACTION_DOWN: {
                 if ((mTouchDispatchState & TOUCH_DISPATCHING_TO_VIEW_IN_PROGRESS) != 0) {
diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
index df6e2c3..c9e80dc 100644
--- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java
+++ b/src/com/android/launcher3/widget/WidgetsDiffReporter.java
@@ -20,8 +20,10 @@
 
 import androidx.recyclerview.widget.RecyclerView;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.icons.IconCache;
 import com.android.launcher3.model.data.PackageItemInfo;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator;
 
 import java.util.ArrayList;
@@ -32,8 +34,8 @@
  * methods accordingly.
  */
 public class WidgetsDiffReporter {
-    private static final boolean DEBUG = false;
-    private static final String TAG = "WidgetsDiffReporter";
+    private static final boolean DEBUG = Utilities.IS_RUNNING_IN_TEST_HARNESS; // b/160238801
+    private static final String TAG = TestProtocol.NO_SCROLL_END_WIDGETS;
 
     private final IconCache mIconCache;
     private final RecyclerView.Adapter mListener;
diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/WidgetsFullSheet.java
index 68a3ec5..ba55f5a 100644
--- a/src/com/android/launcher3/widget/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsFullSheet.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -38,8 +39,10 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHost.ProviderChangedListener;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
+import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.TopRoundedCornerView;
 
@@ -68,6 +71,14 @@
 
     }
 
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsFullSheet: " + ev);
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
     public WidgetsFullSheet(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
index 82d4110..8f81977 100644
--- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java
+++ b/src/com/android/launcher3/widget/WidgetsRecyclerView.java
@@ -120,6 +120,9 @@
     public int getCurrentScrollY() {
         // Skip early if widgets are not bound.
         if (isModelNotReady() || getChildCount() == 0) {
+            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "getCurrentScrollY: -1");
+            }
             return -1;
         }
 
@@ -128,6 +131,10 @@
         int y = (child.getMeasuredHeight() * rowIndex);
         int offset = getLayoutManager().getDecoratedTop(child);
 
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS,
+                    "getCurrentScrollY: " + (getPaddingTop() + y - offset));
+        }
         return getPaddingTop() + y - offset;
     }
 
@@ -158,13 +165,23 @@
                     mScrollbar.isHitInParent(e.getX(), e.getY(), mFastScrollerOffset);
         }
         if (mTouchDownOnScroller) {
-            return mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
+            final boolean result = mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
+            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 1 " + result);
+            }
+            return result;
+        }
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onInterceptTouchEvent 2 false");
         }
         return false;
     }
 
     @Override
     public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView.onTouchEvent");
+        }
         if (mTouchDownOnScroller) {
             mScrollbar.handleTouchEvent(e, mFastScrollerOffset);
         }
@@ -172,5 +189,31 @@
 
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "onRequestDisallowInterceptTouchEvent "
+                    + disallowIntercept);
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        final boolean result = super.dispatchTouchEvent(ev);
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "WidgetsRecyclerView: state: "
+                    + getScrollState()
+                    + " can scroll: " + getLayoutManager().canScrollVertically()
+                    + " result: " + result
+                    + " layout suppressed: " + isLayoutSuppressed()
+                    + " event: " + ev);
+        }
+        return result;
+    }
+
+    @Override
+    public void stopNestedScroll() {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "stopNestedScroll");
+        }
+        super.stopNestedScroll();
     }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
index 202dcb1..dd216c7 100644
--- a/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
+++ b/tests/src/com/android/launcher3/ui/ActivityLeakTracker.java
@@ -73,20 +73,12 @@
     }
 
     public boolean noLeakedActivities() {
-        int liveActivities = 0;
-        int destroyedActivities = 0;
-
         for (Activity activity : mActivities.keySet()) {
             if (activity.isDestroyed()) {
-                ++destroyedActivities;
-            } else {
-                ++liveActivities;
+                return false;
             }
         }
 
-        if (liveActivities > 2) return false;
-
-        // It's OK to have 1 leaked activity if no active activities exist.
-        return liveActivities == 0 ? destroyedActivities <= 1 : destroyedActivities == 0;
+        return mActivities.size() <= 2;
     }
 }
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f4274a8..bf079cb 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -635,9 +635,11 @@
     Parcelable executeAndWaitForEvent(Runnable command,
             UiAutomation.AccessibilityEventFilter eventFilter, Supplier<String> message) {
         try {
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "executeAndWaitForEvent: before");
             final AccessibilityEvent event =
                     mInstrumentation.getUiAutomation().executeAndWaitForEvent(
                             command, eventFilter, WAIT_TIME_MS);
+            Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "executeAndWaitForEvent: after");
             assertNotNull("executeAndWaitForEvent returned null (this can't happen)", event);
             final Parcelable parcelableData = event.getParcelableData();
             event.recycle();
@@ -1094,7 +1096,10 @@
         executeAndWaitForEvent(
                 () -> linearGesture(
                         startX, startY, endX, endY, steps, slowDown, GestureScope.INSIDE),
-                event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
+                event -> {
+                    Log.d(TestProtocol.NO_SCROLL_END_WIDGETS, "scroll: received event: " + event);
+                    return TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName());
+                },
                 () -> "Didn't receive a scroll end message: " + startX + ", " + startY
                         + ", " + endX + ", " + endY);
     }
diff --git a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
index 4440b82..ab6465c 100644
--- a/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
+++ b/tests/tapl/com/android/launcher3/tapl/LogEventChecker.java
@@ -57,6 +57,8 @@
         while (true) {
             rawEvents = mLauncher.getTestInfo(TestProtocol.REQUEST_GET_TEST_EVENTS)
                     .getStringArrayList(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+            if (rawEvents == null) return null;
+
             final int expectedCount = mExpectedEvents.entrySet()
                     .stream().mapToInt(e -> e.getValue().size()).sum();
             if (rawEvents.size() >= expectedCount
@@ -83,6 +85,7 @@
 
     String verify(long waitForExpectedCountMs, boolean successfulGesture) {
         final ListMap<String> actualEvents = finishSync(waitForExpectedCountMs);
+        if (actualEvents == null) return "null event sequences because launcher likely died";
 
         final StringBuilder sb = new StringBuilder();
         boolean hasMismatches = false;
@@ -91,8 +94,7 @@
 
             List<String> actual = new ArrayList<>(actualEvents.getNonNull(sequence));
             final int mismatchPosition = getMismatchPosition(expectedEvents.getValue(), actual);
-            hasMismatches = hasMismatches
-                    || mismatchPosition != -1 && !ignoreMistatch(successfulGesture, sequence);
+            hasMismatches = hasMismatches || mismatchPosition != -1;
             formatSequenceWithMismatch(
                     sb,
                     sequence,
@@ -103,8 +105,7 @@
         // Check for unexpected event sequences in the actual data.
         for (String actualNamedSequence : actualEvents.keySet()) {
             if (!mExpectedEvents.containsKey(actualNamedSequence)) {
-                hasMismatches = hasMismatches
-                        || !ignoreMistatch(successfulGesture, actualNamedSequence);
+                hasMismatches = true;
                 formatSequenceWithMismatch(
                         sb,
                         actualNamedSequence,
@@ -117,13 +118,6 @@
         return hasMismatches ? "mismatching events: " + sb.toString() : null;
     }
 
-    // Workaround for b/154157191
-    private static boolean ignoreMistatch(boolean successfulGesture, String sequence) {
-        // b/156287114
-        return false;
-//        return TestProtocol.SEQUENCE_TIS.equals(sequence) && successfulGesture;
-    }
-
     // If the list of actual events matches the list of expected events, returns -1, otherwise
     // the position of the mismatch.
     private static int getMismatchPosition(List<Pattern> expected, List<String> actual) {
diff --git a/tests/tapl/com/android/launcher3/tapl/Widgets.java b/tests/tapl/com/android/launcher3/tapl/Widgets.java
index 39ac645..49af616 100644
--- a/tests/tapl/com/android/launcher3/tapl/Widgets.java
+++ b/tests/tapl/com/android/launcher3/tapl/Widgets.java
@@ -28,6 +28,7 @@
 import androidx.test.uiautomator.Until;
 
 import com.android.launcher3.tapl.LauncherInstrumentation.GestureScope;
+import com.android.launcher3.testing.TestProtocol;
 
 import java.util.Collection;
 
@@ -90,6 +91,12 @@
         return LauncherInstrumentation.ContainerType.WIDGETS;
     }
 
+    private int getWidgetsScroll() {
+        return mLauncher.getTestInfo(
+                TestProtocol.REQUEST_WIDGETS_SCROLL_Y)
+                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+    }
+
     public Widget getWidget(String labelText) {
         try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
              LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
@@ -136,7 +143,13 @@
                 }
 
                 mLauncher.assertTrue("Too many attempts", ++i <= 40);
+                final int scroll = getWidgetsScroll();
                 mLauncher.scrollToLastVisibleRow(widgetsContainer, cells, 0);
+                final int newScroll = getWidgetsScroll();
+                mLauncher.assertTrue(
+                        "Scrolled in a wrong direction in Widgets: from " + scroll + " to "
+                                + newScroll, newScroll >= scroll);
+                mLauncher.assertTrue("Unable to scroll to the widget", newScroll != scroll);
             }
         }
     }