Merge "Adding feature to automatically show keyboard in all-apps" into tm-dev
diff --git a/quickstep/src/com/android/launcher3/model/AppEventProducer.java b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
index 5c66944..9f3be69 100644
--- a/quickstep/src/com/android/launcher3/model/AppEventProducer.java
+++ b/quickstep/src/com/android/launcher3/model/AppEventProducer.java
@@ -62,6 +62,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.launcher3.Utilities;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logger.LauncherAtom.ContainerInfo;
 import com.android.launcher3.logger.LauncherAtom.FolderContainer;
@@ -116,7 +117,8 @@
     @AnyThread
     private void sendEvent(AppTarget target, LauncherAtom.ItemInfo locationInfo, int eventId,
             int targetPredictor) {
-        if (target != null) {
+        // TODO: remove the running test check when b/231648228 is fixed.
+        if (target != null && !Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             AppTargetEvent event = new AppTargetEvent.Builder(target, eventId)
                     .setLaunchLocation(getContainer(locationInfo))
                     .build();
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 349dd0a..cbdbdb9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.taskbar;
 
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
 import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
@@ -51,6 +52,7 @@
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.PaintDrawable;
 import android.inputmethodservice.InputMethodService;
+import android.os.Handler;
 import android.util.Property;
 import android.view.Gravity;
 import android.view.MotionEvent;
@@ -158,6 +160,7 @@
     private BaseDragLayer<TaskbarActivityContext> mSeparateWindowParent; // Initialized in init.
     private final ViewTreeObserverWrapper.OnComputeInsetsListener mSeparateWindowInsetsComputer =
             this::onComputeInsetsForSeparateWindow;
+    private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
 
     public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
         mContext = context;
@@ -297,6 +300,8 @@
                 navButtonsLayoutParams.setMarginEnd(navButtonsLayoutParams.getMarginStart());
                 navButtonsLayoutParams.gravity = Gravity.CENTER;
                 mNavButtonContainer.requestLayout();
+
+                mHomeButton.setOnLongClickListener(null);
             }
 
             // Animate taskbar background when either..
@@ -388,8 +393,7 @@
                         || (flags & FLAG_KEYGUARD_VISIBLE) != 0,
                 VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0));
 
-
-        // home and recents buttons
+        // home button
         mHomeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer,
                 navButtonController, R.id.home);
         mHomeButtonAlpha = new MultiValueAlpha(mHomeButton, NUM_ALPHA_CHANNELS);
@@ -399,8 +403,21 @@
                         ALPHA_INDEX_KEYGUARD_OR_DISABLE),
                 flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 &&
                         (flags & FLAG_DISABLE_HOME) == 0));
+
+        // Recents button
         View recentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS,
                 navContainer, navButtonController, R.id.recent_apps);
+        mHitboxExtender.init(recentsButton, mNavButtonsView, mContext.getDeviceProfile(),
+                () -> {
+                    float[] recentsCoords = new float[2];
+                    getDescendantCoordRelativeToAncestor(recentsButton, mNavButtonsView,
+                            recentsCoords, false);
+                    return recentsCoords;
+                }, new Handler());
+        recentsButton.setOnClickListener(v -> {
+            navButtonController.onButtonClick(BUTTON_RECENTS);
+            mHitboxExtender.onRecentsButtonClicked();
+        });
         mPropertyHolders.add(new StatePropertyHolder(recentsButton,
                 flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_RECENTS) == 0
                         && !mContext.isNavBarKidsModeActive()));
@@ -504,6 +521,9 @@
             View button = mAllButtons.get(i);
             if (button.getVisibility() == View.VISIBLE) {
                 parent.getDescendantRectRelativeToSelf(button, mTempRect);
+                if (mHitboxExtender.extendedHitboxEnabled()) {
+                    mTempRect.bottom += mContext.mDeviceProfile.getTaskbarOffsetY();
+                }
                 outRegion.op(mTempRect, Op.UNION);
             }
         }
@@ -733,6 +753,17 @@
         return str.toString();
     }
 
+    public TouchController getTouchController() {
+        return mHitboxExtender;
+    }
+
+    /**
+     * @param alignment 0 -> Taskbar, 1 -> Workspace
+     */
+    public void updateTaskbarAlignment(float alignment) {
+        mHitboxExtender.onAnimationProgressToOverview(alignment);
+    }
+
     private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback {
         @Override
         public void onVisibilityChanged(boolean isVisible) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java b/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java
new file mode 100644
index 0000000..4651570
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar;
+
+import android.graphics.Rect;
+import android.os.Handler;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.util.TouchController;
+
+import java.util.function.Supplier;
+
+/**
+ * Extends the Recents touch area during the taskbar to overview animation
+ * to give user some error room when trying to quickly double tap recents button since it moves.
+ *
+ * Listens for icon alignment as our indication for the animation.
+ */
+public class RecentsHitboxExtender implements TouchController {
+
+    private static final int RECENTS_HITBOX_TIMEOUT_MS = 500;
+
+    private View mRecentsButton;
+    private View mRecentsParent;
+    private DeviceProfile mDeviceProfile;
+    private Supplier<float[]> mParentCoordSupplier;
+    private TouchDelegate mRecentsTouchDelegate;
+    /**
+     * Will be true while the animation from taskbar to overview is occurring.
+     * Lifecycle of this variable slightly extends past the animation by
+     * {@link #RECENTS_HITBOX_TIMEOUT_MS}, so can use this variable as a proxy for if
+     * the current hitbox is extended or not.
+     */
+    private boolean mAnimatingFromTaskbarToOverview;
+    private float mLastIconAlignment;
+    private final Rect mRecentsHitBox = new Rect();
+    private boolean mRecentsButtonClicked;
+    private Handler mHandler;
+    private final Runnable mRecentsHitboxResetRunnable = this::reset;
+
+    public void init(View recentsButton, View recentsParent, DeviceProfile deviceProfile,
+            Supplier<float[]> parentCoordSupplier, Handler handler) {
+        mRecentsButton = recentsButton;
+        mRecentsParent = recentsParent;
+        mDeviceProfile = deviceProfile;
+        mParentCoordSupplier = parentCoordSupplier;
+        mHandler = handler;
+    }
+
+    public void onRecentsButtonClicked() {
+        mRecentsButtonClicked = true;
+    }
+
+    /**
+     * @param progress 0 -> Taskbar, 1 -> Overview
+     */
+    public void onAnimationProgressToOverview(float progress) {
+        if (progress == 1 || progress == 0) {
+            // Done w/ animation
+            mLastIconAlignment = progress;
+            if (mAnimatingFromTaskbarToOverview) {
+                if (progress == 1) {
+                    // Finished animation to workspace, remove the touch delegate shortly
+                    mHandler.postDelayed(mRecentsHitboxResetRunnable, RECENTS_HITBOX_TIMEOUT_MS);
+                    return;
+                } else {
+                    // Went back to taskbar, reset immediately
+                    mHandler.removeCallbacks(mRecentsHitboxResetRunnable);
+                    reset();
+                }
+            }
+        }
+
+        if (mAnimatingFromTaskbarToOverview) {
+            return;
+        }
+
+        if (progress > 0 && mLastIconAlignment == 0 && mRecentsButtonClicked) {
+            // Starting animation, previously we were showing taskbar
+            mAnimatingFromTaskbarToOverview = true;
+            float[] recentsCoords = mParentCoordSupplier.get();
+            int x = (int) recentsCoords[0];
+            int y = (int) (recentsCoords[1]);
+            // Extend hitbox vertically by the offset amount from mDeviceProfile.getTaskbarOffsetY()
+            mRecentsHitBox.set(x, y,
+                    x + mRecentsButton.getWidth(),
+                    y + mRecentsButton.getHeight() + mDeviceProfile.getTaskbarOffsetY()
+            );
+            mRecentsTouchDelegate = new TouchDelegate(mRecentsHitBox, mRecentsButton);
+            mRecentsParent.setTouchDelegate(mRecentsTouchDelegate);
+        }
+    }
+
+    private void reset() {
+        mAnimatingFromTaskbarToOverview = false;
+        mRecentsButton.setTouchDelegate(null);
+        mRecentsHitBox.setEmpty();
+        mRecentsButtonClicked = false;
+    }
+
+    /**
+     * @return {@code true} if the bounds for recents touches are currently extended
+     */
+    public boolean extendedHitboxEnabled() {
+        return mAnimatingFromTaskbarToOverview;
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return mRecentsTouchDelegate.onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        return mRecentsHitBox.contains((int)ev.getX(), (int)ev.getY());
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 3e2695c..99c59a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -182,7 +182,8 @@
          */
         public TouchController[] getTouchControllers() {
             return new TouchController[]{mActivity.getDragController(),
-                    mControllers.taskbarForceVisibleImmersiveController};
+                    mControllers.taskbarForceVisibleImmersiveController,
+                    mControllers.navbarButtonsViewController.getTouchController()};
         }
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 138fb99..052c695 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -372,6 +372,7 @@
 
         // If we're already animating to the value, just leave it be instead of restarting it.
         if (!mIconAlignmentForLauncherState.isAnimatingToValue(toAlignment)) {
+            mIconAlignmentForLauncherState.finishAnimation();
             animatorSet.play(mIconAlignmentForLauncherState.animateToValue(toAlignment)
                     .setDuration(duration));
         }
@@ -427,6 +428,7 @@
 
         // Switch taskbar and hotseat in last frame
         setTaskbarViewVisible(alignment < 1);
+        mControllers.navbarButtonsViewController.updateTaskbarAlignment(alignment);
     }
 
     private float getCurrentIconAlignmentRatioBetweenAppAndHome() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 2ca59eb..53dc9dd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -27,6 +27,7 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_QUICKSWITCH_RIGHT;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEDOWN;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_UNKNOWN_SWIPEUP;
 import static com.android.launcher3.logging.StatsLogManager.getLauncherAtomEvent;
@@ -451,9 +452,11 @@
                 .withSrcState(LAUNCHER_STATE_HOME)
                 .withDstState(targetState.statsLogOrdinal)
                 .log(getLauncherAtomEvent(mStartState.statsLogOrdinal, targetState.statsLogOrdinal,
-                        targetState.ordinal > mStartState.ordinal
-                                ? LAUNCHER_UNKNOWN_SWIPEUP
-                                : LAUNCHER_UNKNOWN_SWIPEDOWN));
+                        targetState == QUICK_SWITCH
+                                ? LAUNCHER_QUICKSWITCH_RIGHT
+                                : targetState.ordinal > mStartState.ordinal
+                                        ? LAUNCHER_UNKNOWN_SWIPEUP
+                                        : LAUNCHER_UNKNOWN_SWIPEDOWN));
         mLauncher.getStateManager().goToState(targetState, false, forEndCallback(this::clearState));
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 1504c12..dbee9c1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -261,7 +261,7 @@
 
     @Override
     protected void onReachedFinalState(LauncherState toState) {
-        super.onReinitToState(toState);
+        super.onReachedFinalState(toState);
         if (toState == ALL_APPS) {
             InteractionJankMonitorWrapper.end(InteractionJankMonitorWrapper.CUJ_OPEN_ALL_APPS);
         }
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 5acce89..f60b225 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -22,6 +22,7 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.PagedView.INVALID_PAGE;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
@@ -66,6 +67,7 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
@@ -926,7 +928,13 @@
             mLogDirectionUpOrLeft = velocity.x < 0;
         }
         mDownPos = downPos;
-        handleNormalGestureEnd(endVelocity, isFling, velocity, false /* isCancel */);
+        Runnable handleNormalGestureEndCallback = () ->
+                handleNormalGestureEnd(endVelocity, isFling, velocity, /* isCancel= */ false);
+        if (mRecentsView != null) {
+            mRecentsView.runOnPageScrollsInitialized(handleNormalGestureEndCallback);
+        } else {
+            handleNormalGestureEndCallback.run();
+        }
     }
 
     private void endRunningWindowAnim(boolean cancel) {
@@ -1130,6 +1138,13 @@
         } else if (endTarget == RECENTS) {
             if (mRecentsView != null) {
                 int nearestPage = mRecentsView.getDestinationPage();
+                if (nearestPage == INVALID_PAGE) {
+                    // Allow the snap to invalid page to catch future error cases.
+                    Log.e(TAG,
+                            "RecentsView destination page is invalid",
+                            new IllegalStateException());
+                }
+
                 boolean isScrolling = false;
                 if (mRecentsView.getNextPage() != nearestPage) {
                     // We shouldn't really scroll to the next page when swiping up to recents.
@@ -1162,6 +1177,11 @@
     }
 
     private void doLogGesture(GestureEndTarget endTarget, @Nullable TaskView targetTask) {
+        if (mDp == null || !mDp.isGestureMode || mDownPos == null) {
+            // We probably never received an animation controller, skip logging.
+            return;
+        }
+
         StatsLogManager.EventEnum event;
         switch (endTarget) {
             case HOME:
@@ -1185,11 +1205,6 @@
             logger.withItemInfo(targetTask.getItemInfo());
         }
 
-        DeviceProfile dp = mDp;
-        if (dp == null || mDownPos == null) {
-            // We probably never received an animation controller, skip logging.
-            return;
-        }
         int pageIndex = endTarget == LAST_TASK || mRecentsView == null
                 ? LOG_NO_OP_PAGE_INDEX
                 : mRecentsView.getNextPage();
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 48127c0..6745246 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -84,6 +84,8 @@
 
     private STATE_TYPE mTargetState;
 
+    @Nullable private Runnable mOnInitBackgroundStateUICallback = null;
+
     protected BaseActivityInterface(boolean rotationSupportedByActivity,
             STATE_TYPE overviewState, STATE_TYPE backgroundState) {
         this.rotationSupportedByActivity = rotationSupportedByActivity;
@@ -408,6 +410,21 @@
         return null;
     }
 
+    protected void runOnInitBackgroundStateUI(Runnable callback) {
+        mOnInitBackgroundStateUICallback = callback;
+        ACTIVITY_TYPE activity = getCreatedActivity();
+        if (activity != null && activity.getStateManager().getState() == mBackgroundState) {
+            onInitBackgroundStateUI();
+        }
+    }
+
+    private void onInitBackgroundStateUI() {
+        if (mOnInitBackgroundStateUICallback != null) {
+            mOnInitBackgroundStateUICallback.run();
+            mOnInitBackgroundStateUICallback = null;
+        }
+    }
+
     public interface AnimationFactory {
 
         void createActivityInterface(long transitionLength);
@@ -447,13 +464,14 @@
             mStartState = mActivity.getStateManager().getState();
         }
 
-        protected ACTIVITY_TYPE initUI() {
+        protected ACTIVITY_TYPE initBackgroundStateUI() {
             STATE_TYPE resetState = mStartState;
             if (mStartState.shouldDisableRestore()) {
                 resetState = mActivity.getStateManager().getRestState();
             }
             mActivity.getStateManager().setRestState(resetState);
             mActivity.getStateManager().goToState(mBackgroundState, false);
+            onInitBackgroundStateUI();
             return mActivity;
         }
 
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 7feec2c..ba61574 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -89,7 +89,7 @@
             boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback) {
         notifyRecentsOfOrientation(deviceState.getRotationTouchHelper());
         DefaultAnimationFactory factory = new DefaultAnimationFactory(callback);
-        factory.initUI();
+        factory.initBackgroundStateUI();
         return factory;
     }
 
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 10a3a2e..c13b95f 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -135,7 +135,7 @@
             }
         };
 
-        BaseQuickstepLauncher launcher = factory.initUI();
+        BaseQuickstepLauncher launcher = factory.initBackgroundStateUI();
         // Since all apps is not visible, we can safely reset the scroll position.
         // This ensures then the next swipe up to all-apps starts from scroll 0.
         launcher.getAppsView().reset(false /* animate */);
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 42f9eb6..dffdc5a 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -217,7 +217,8 @@
             @Override
             public void onRecentsAnimationStart(RecentsAnimationController controller,
                     RecentsAnimationTargets targets) {
-                interactionHandler.onGestureEnded(0, new PointF(), new PointF());
+                activityInterface.runOnInitBackgroundStateUI(() ->
+                        interactionHandler.onGestureEnded(0, new PointF(), new PointF()));
                 cmd.removeListener(this);
             }
 
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index ef81449..528fb97 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -66,6 +66,12 @@
                         mDeviceProfile.overviewPageSpacing);
                 return response;
             }
+
+            case TestProtocol.REQUEST_HAS_TIS: {
+                response.putBoolean(
+                        TestProtocol.REQUEST_HAS_TIS, true);
+                return response;
+            }
         }
 
         return super.call(method, arg, extras);
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 2565674..54f457d 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -192,6 +192,10 @@
                                 RemoteAnimationTargetCompat.wrap(nonAppTargets) /* nonApps */);
                         return;
                     }
+                } else if (nonAppTargets != null && nonAppTargets.length > 0) {
+                    TaskViewUtils.createSplitAuxiliarySurfacesAnimator(
+                            RemoteAnimationTargetCompat.wrap(nonAppTargets) /* nonApps */,
+                            true /*shown*/, dividerAnimator -> dividerAnimator.start());
                 }
                 if (mController != null) {
                     if (mLastAppearedTaskTarget == null
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index 10c56c9..85ef6cb 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -130,7 +130,7 @@
         if (IS_VERBOSE) {
             Log.d(TAG, String.format("\nwriteSnapshot(%d):\n%s", instanceId.getId(), info));
         }
-        if (!Utilities.ATLEAST_R) {
+        if (!Utilities.ATLEAST_R || Utilities.IS_RUNNING_IN_TEST_HARNESS) {
             return;
         }
         SysUiStatsLog.write(SysUiStatsLog.LAUNCHER_SNAPSHOT,
@@ -415,6 +415,10 @@
                 consumer.consume(event, atomInfo);
             }
 
+            // TODO: remove this when b/231648228 is fixed.
+            if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+                return;
+            }
             SysUiStatsLog.write(
                     SysUiStatsLog.LAUNCHER_EVENT,
                     SysUiStatsLog.LAUNCHER_UICHANGED__ACTION__DEFAULT_ACTION /* deprecated */,
@@ -510,6 +514,9 @@
     }
 
     private static int getCardinality(LauncherAtom.ItemInfo info) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            return 0;
+        }
         switch (info.getContainerInfo().getContainerCase()) {
             case PREDICTED_HOTSEAT_CONTAINER:
                 return info.getContainerInfo().getPredictedHotseatContainer().getCardinality();
@@ -625,6 +632,9 @@
     }
 
     private static int getHierarchy(LauncherAtom.ItemInfo info) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            return 0;
+        }
         if (info.getContainerInfo().getContainerCase() == FOLDER) {
             return info.getContainerInfo().getFolder().getParentContainerCase().getNumber()
                     + FOLDER_HIERARCHY_OFFSET;
@@ -665,6 +675,9 @@
     }
 
     private static int getSearchAttributes(LauncherAtom.ItemInfo info) {
+        if (Utilities.IS_RUNNING_IN_TEST_HARNESS) {
+            return 0;
+        }
         ContainerInfo containerInfo = info.getContainerInfo();
         if (containerInfo.getContainerCase() == EXTENDED_CONTAINERS
                 && containerInfo.getExtendedContainers().getContainerCase()
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index d4c5b8f..b634518 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -946,7 +946,7 @@
                 mHasVisibleTaskData.delete(i);
             }
             if (child instanceof GroupedTaskView) {
-                mGroupedTaskViewPool.recycle((GroupedTaskView)taskView);
+                mGroupedTaskViewPool.recycle((GroupedTaskView) taskView);
             } else {
                 mTaskViewPool.recycle(taskView);
             }
@@ -1855,17 +1855,18 @@
         if (!mActivity.getDeviceProfile().isTablet) {
             return super.getDestinationPage(scaledScroll);
         }
-
-        final int childCount = getChildCount();
-        if (mPageScrolls == null || childCount != mPageScrolls.length) {
-            return -1;
+        if (!pageScrollsInitialized()) {
+            Log.e(TAG,
+                    "Cannot get destination page: RecentsView not properly initialized",
+                    new IllegalStateException());
+            return INVALID_PAGE;
         }
 
         // When in tablet with variable task width, return the page which scroll is closest to
         // screenStart instead of page nearest to center of screen.
         int minDistanceFromScreenStart = Integer.MAX_VALUE;
-        int minDistanceFromScreenStartIndex = -1;
-        for (int i = 0; i < childCount; ++i) {
+        int minDistanceFromScreenStartIndex = INVALID_PAGE;
+        for (int i = 0; i < getChildCount(); ++i) {
             int distanceFromScreenStart = Math.abs(mPageScrolls[i] - scaledScroll);
             if (distanceFromScreenStart < minDistanceFromScreenStart) {
                 minDistanceFromScreenStart = distanceFromScreenStart;
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java
new file mode 100644
index 0000000..929bff3
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java
@@ -0,0 +1,125 @@
+package com.android.launcher3.taskbar;
+
+import static android.view.MotionEvent.ACTION_DOWN;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Handler;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.DeviceProfile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Supplier;
+
+@RunWith(AndroidJUnit4.class)
+public class RecentsHitboxExtenderTest {
+
+    private static final int TASKBAR_OFFSET_Y = 35;
+    private static final int BUTTON_WIDTH = 10;
+    private static final int BUTTON_HEIGHT = 10;
+
+    private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
+    @Mock
+    View mMockRecentsButton;
+    @Mock
+    View mMockRecentsParent;
+    @Mock
+    DeviceProfile mMockDeviceProfile;
+    @Mock
+    Handler mMockHandler;
+    Context mContext;
+
+    float[] mRecentsCoords = new float[]{0,0};
+    private final Supplier<float[]> mSupplier = () -> mRecentsCoords;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        mContext = instrumentation.getContext();
+        mHitboxExtender.init(mMockRecentsButton, mMockRecentsParent, mMockDeviceProfile, mSupplier,
+                mMockHandler);
+        when(mMockDeviceProfile.getTaskbarOffsetY()).thenReturn(TASKBAR_OFFSET_Y);
+        when(mMockRecentsButton.getContext()).thenReturn(mContext);
+        when(mMockRecentsButton.getWidth()).thenReturn(BUTTON_WIDTH);
+        when(mMockRecentsButton.getHeight()).thenReturn(BUTTON_HEIGHT);
+    }
+
+    @Test
+    public void noRecentsButtonClick_notActive() {
+        mHitboxExtender.onAnimationProgressToOverview(0);
+        mHitboxExtender.onAnimationProgressToOverview(0.5f);
+        assertFalse(mHitboxExtender.extendedHitboxEnabled());
+    }
+
+    @Test
+    public void recentsButtonClick_active() {
+        mHitboxExtender.onRecentsButtonClicked();
+        mHitboxExtender.onAnimationProgressToOverview(0);
+        mHitboxExtender.onAnimationProgressToOverview(0.5f);
+        assertTrue(mHitboxExtender.extendedHitboxEnabled());
+    }
+
+    @Test
+    public void homeToTaskbar_notActive() {
+        mHitboxExtender.onAnimationProgressToOverview(1);
+        mHitboxExtender.onAnimationProgressToOverview(0.5f);
+        assertFalse(mHitboxExtender.extendedHitboxEnabled());
+    }
+
+    @Test
+    public void animationEndReset() {
+        mHitboxExtender.onRecentsButtonClicked();
+        mHitboxExtender.onAnimationProgressToOverview(0);
+        mHitboxExtender.onAnimationProgressToOverview(0.5f);
+        assertTrue(mHitboxExtender.extendedHitboxEnabled());
+        mHitboxExtender.onAnimationProgressToOverview(1);
+        verify(mMockHandler, times(1)).postDelayed(any(), anyLong());
+    }
+
+    @Test
+    public void motionWithinHitbox() {
+        mHitboxExtender.onRecentsButtonClicked();
+        mHitboxExtender.onAnimationProgressToOverview(0);
+        mHitboxExtender.onAnimationProgressToOverview(0.5f);
+        assertTrue(mHitboxExtender.extendedHitboxEnabled());
+        // Center width, past height but w/in offset bounds
+        MotionEvent motionEvent = getMotionEvent(ACTION_DOWN,
+                BUTTON_WIDTH / 2, BUTTON_HEIGHT + TASKBAR_OFFSET_Y / 2);
+        assertTrue(mHitboxExtender.onControllerInterceptTouchEvent(motionEvent));
+    }
+
+    @Test
+    public void motionOutsideHitbox() {
+        mHitboxExtender.onRecentsButtonClicked();
+        mHitboxExtender.onAnimationProgressToOverview(0);
+        mHitboxExtender.onAnimationProgressToOverview(0.5f);
+        assertTrue(mHitboxExtender.extendedHitboxEnabled());
+        // Center width, past height and offset
+        MotionEvent motionEvent = getMotionEvent(ACTION_DOWN,
+                BUTTON_WIDTH / 2, BUTTON_HEIGHT + TASKBAR_OFFSET_Y * 2);
+        assertFalse(mHitboxExtender.onControllerInterceptTouchEvent(motionEvent));
+    }
+
+    private MotionEvent getMotionEvent(int action, int x, int y) {
+        return MotionEvent.obtain(0, 0, action, x, y, 0);
+    }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 4d38822..4bf247c 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -52,6 +52,8 @@
 public class TaplTestsQuickstep extends AbstractQuickStepTest {
 
     private static final String APP_NAME = "LauncherTestApp";
+    private static final String CALCULATOR_APP_PACKAGE =
+            resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
 
     @Before
     public void setUp() throws Exception {
@@ -73,7 +75,7 @@
 
     public static void startTestApps() throws Exception {
         startAppFast(getAppPackageName());
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(CALCULATOR_APP_PACKAGE);
         startTestActivity(2);
     }
 
@@ -207,7 +209,7 @@
     @NavigationModeSwitch
     @PortraitLandscape
     public void testBackground() throws Exception {
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(CALCULATOR_APP_PACKAGE);
         final LaunchedAppState launchedAppState = getAndAssertLaunchedApp();
 
         assertNotNull("Background.switchToOverview() returned null",
@@ -302,7 +304,7 @@
         mLauncher.getWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
 
-        startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
+        startAppFast(CALCULATOR_APP_PACKAGE);
         mLauncher.pressBack();
         mLauncher.getWorkspace();
         waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index b9942c0..81e94f7 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -18,10 +18,11 @@
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:shape="rectangle">
+    <solid android:color="?androidprv:attr/colorSurfaceVariant" />
     <corners android:radius="@dimen/rounded_button_radius" />
     <stroke
         android:width="1dp"
-        android:color="?androidprv:attr/colorAccentPrimaryVariant" />
+        android:color="?androidprv:attr/colorSurfaceVariant" />
     <padding
         android:left="@dimen/rounded_button_padding"
         android:right="@dimen/rounded_button_padding" />
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 0f6fc6c..feebfe1 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -22,7 +22,7 @@
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:layout_weight="1"
-        android:importantForAccessibility="no"
+        android:importantForAccessibility="noHideDescendants"
         android:layout_marginVertical="8dp">
         <!-- The image of the widget. This view does not support padding. Any placement adjustment
              should be done using margins. Width & height are set at runtime after scaling the
diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml
index 8f0eae7..3cdc2e8 100644
--- a/res/layout/widgets_list_row_header.xml
+++ b/res/layout/widgets_list_row_header.xml
@@ -21,7 +21,10 @@
     android:layout_height="wrap_content"
     android:paddingVertical="@dimen/widget_list_header_view_vertical_padding"
     android:orientation="horizontal"
-    launcher:appIconSize="48dp">
+    android:importantForAccessibility="yes"
+    android:focusable="true"
+    launcher:appIconSize="48dp"
+    android:descendantFocusability="afterDescendants">
 
     <ImageView
         android:id="@+id/app_icon"
@@ -32,14 +35,11 @@
         tools:src="@drawable/ic_corp"/>
 
     <LinearLayout
-        android:id="@+id/app_container"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
         android:layout_weight="1"
-        android:orientation="vertical"
-        android:focusable="true"
-        android:descendantFocusability="afterDescendants">
+        android:orientation="vertical">
 
         <TextView
             android:id="@+id/app_title"
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index 73200de..f3b3053 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -16,9 +16,9 @@
 <com.android.launcher3.allapps.WorkEduCard xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginTop="8dp"
+    android:layout_marginTop="@dimen/work_edu_card_margin"
+    android:layout_marginBottom="@dimen/work_edu_card_bottom_margin"
     android:gravity="center">
-
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -36,30 +36,29 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginBottom="@dimen/work_card_margin"
-            android:layout_marginRight="@dimen/work_card_margin"
+            android:layout_marginEnd="@dimen/work_card_margin"
             android:text="@string/work_profile_edu_work_apps"
+            android:textDirection="locale"
             android:textSize="18sp" />
         <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="@dimen/padded_rounded_button_height"
-            android:orientation="horizontal"
-            >
+            android:orientation="horizontal">
             <FrameLayout
                 android:layout_width="@dimen/rounded_button_width"
                 android:layout_height="@dimen/rounded_button_width"
+                android:layout_alignParentEnd="true"
                 android:background="@drawable/rounded_action_button"
-                android:padding="@dimen/rounded_button_padding"
-                android:layout_alignParentRight="true">
+                android:padding="@dimen/rounded_button_padding">
                 <ImageButton
+                    android:id="@+id/action_btn"
                     android:layout_width="@dimen/x_icon_size"
                     android:layout_height="@dimen/x_icon_size"
-                    android:id="@+id/action_btn"
-                    android:src="@drawable/ic_remove_no_shadow"
                     android:layout_gravity="center"
-                    android:padding="@dimen/x_icon_padding" />
+                    android:padding="@dimen/x_icon_padding"
+                    android:src="@drawable/ic_remove_no_shadow" />
             </FrameLayout>
         </RelativeLayout>
-
     </RelativeLayout>
 
 
diff --git a/res/layout/work_mode_fab.xml b/res/layout/work_mode_fab.xml
index c536d77..d2fa5fa 100644
--- a/res/layout/work_mode_fab.xml
+++ b/res/layout/work_mode_fab.xml
@@ -21,14 +21,14 @@
     android:layout_width="wrap_content"
     android:gravity="center"
     android:includeFontPadding="false"
+    android:textDirection="locale"
     android:drawableTint="@color/all_apps_tab_text"
     android:textColor="@color/all_apps_tab_text"
     android:textSize="14sp"
     android:background="@drawable/work_apps_toggle_background"
     android:drawablePadding="8dp"
     android:drawableStart="@drawable/ic_corp_off"
-    android:layout_marginBottom="@dimen/work_fab_margin"
-    android:layout_marginEnd="@dimen/work_fab_margin"
+    android:layout_marginBottom="@dimen/work_fab_margin_bottom"
     android:paddingLeft="@dimen/work_mode_fab_padding"
     android:paddingRight="@dimen/work_mode_fab_padding"
     android:text="@string/work_apps_pause_btn_text" />
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 2c3f5ed..a8ee721 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -129,6 +129,7 @@
     <dimen name="all_apps_height_extra">6dp</dimen>
     <dimen name="all_apps_bottom_sheet_horizontal_padding">0dp</dimen>
     <dimen name="all_apps_paged_view_top_padding">40dp</dimen>
+    <dimen name="all_apps_personal_work_tabs_vertical_margin">16dp</dimen>
 
     <!-- The size of corner radius of the arrow in the arrow toast. -->
     <dimen name="arrow_toast_corner_radius">2dp</dimen>
@@ -148,10 +149,12 @@
     <dimen name="work_card_padding_horizontal">10dp</dimen>
     <dimen name="work_card_button_height">52dp</dimen>
     <dimen name="work_fab_margin">16dp</dimen>
+    <dimen name="work_fab_margin_bottom">20dp</dimen>
     <dimen name="work_mode_fab_padding">16dp</dimen>
     <dimen name="work_profile_footer_padding">20dp</dimen>
     <dimen name="work_edu_card_margin">16dp</dimen>
     <dimen name="work_edu_card_radius">16dp</dimen>
+    <dimen name="work_edu_card_bottom_margin">26dp</dimen>
 
     <dimen name="work_card_margin">24dp</dimen>
     <!-- (x) icon button inside work edu card -->
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ffa1e3f..ee5e024 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -428,7 +428,7 @@
     <string name="work_apps_paused_edu_accept">Got it</string>
 
     <!-- button string shown pause work profile -->
-    <string name="work_apps_pause_btn_text">Turn off work apps</string>
+    <string name="work_apps_pause_btn_text">Pause work apps</string>
     <!-- button string shown enable work profile -->
     <string name="work_apps_enable_btn_text">Turn on work apps</string>
 
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2ad1d47..ad87451 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1279,13 +1279,16 @@
      * @param info The data structure describing the shortcut.
      */
     View createShortcut(WorkspaceItemInfo info) {
-        return createShortcut((ViewGroup) mWorkspace.getChildAt(mWorkspace.getCurrentPage()), info);
+        // This can be called before PagedView#pageScrollsInitialized returns true, so use the
+        // first page, which we always assume to be present.
+        return createShortcut((ViewGroup) mWorkspace.getChildAt(0), info);
     }
 
     /**
      * Creates a view representing a shortcut inflated from the specified resource.
      *
-     * @param parent The group the shortcut belongs to.
+     * @param parent The group the shortcut belongs to. This is not necessarily the group where
+     *               the shortcut should be added.
      * @param info   The data structure describing the shortcut.
      * @return A View inflated from layoutResId.
      */
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 4501159..597bc8d 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
+
 import static com.android.launcher3.Utilities.getDevicePrefs;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_THEMED_ICONS;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -97,7 +99,8 @@
         modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
                 Intent.ACTION_MANAGED_PROFILE_AVAILABLE,
                 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE,
-                Intent.ACTION_MANAGED_PROFILE_UNLOCKED);
+                Intent.ACTION_MANAGED_PROFILE_UNLOCKED,
+                ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
         if (FeatureFlags.IS_STUDIO_BUILD) {
             modelChangeReceiver.register(mContext, Context.RECEIVER_EXPORTED, ACTION_FORCE_ROLOAD);
         }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index ee6f51e..5c5c101 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3;
 
+import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
+
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -51,6 +53,7 @@
 import com.android.launcher3.model.PackageIncrementalDownloadUpdatedTask;
 import com.android.launcher3.model.PackageInstallStateChangedTask;
 import com.android.launcher3.model.PackageUpdatedTask;
+import com.android.launcher3.model.ReloadStringCacheTask;
 import com.android.launcher3.model.ShortcutsChangedTask;
 import com.android.launcher3.model.UserLockStateChangedTask;
 import com.android.launcher3.model.data.AppInfo;
@@ -278,6 +281,8 @@
                             user, Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)));
                 }
             }
+        } else if (ACTION_DEVICE_POLICY_RESOURCE_UPDATED.equals(action)) {
+            enqueueModelUpdateTask(new ReloadStringCacheTask(mModelDelegate));
         } else if (IS_STUDIO_BUILD && ACTION_FORCE_ROLOAD.equals(action)) {
             for (Callbacks cb : getCallbacks()) {
                 if (cb instanceof Launcher) {
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 0a1d25c..95a8a2a 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -129,7 +129,10 @@
     private boolean mAllowEasyFling;
     protected PagedOrientationHandler mOrientationHandler = PagedOrientationHandler.PORTRAIT;
 
-    protected int[] mPageScrolls;
+    private final ArrayList<Runnable> mOnPageScrollsInitializedCallbacks = new ArrayList<>();
+
+    // We should always check pageScrollsInitialized() is true when using mPageScrolls.
+    @Nullable protected int[] mPageScrolls = null;
     private boolean mIsBeingDragged;
 
     // The amount of movement to begin scrolling
@@ -684,14 +687,37 @@
         setMeasuredDimension(widthSize, heightSize);
     }
 
+    /** Returns true iff this PagedView's scroll amounts are initialized to each page index. */
+    protected boolean pageScrollsInitialized() {
+        return mPageScrolls != null && mPageScrolls.length == getChildCount();
+    }
+
+    /**
+     * Queues the given callback to be run once {@code mPageScrolls} has been initialized.
+     */
+    public void runOnPageScrollsInitialized(Runnable callback) {
+        mOnPageScrollsInitializedCallbacks.add(callback);
+        if (pageScrollsInitialized()) {
+            onPageScrollsInitialized();
+        }
+    }
+
+    private void onPageScrollsInitialized() {
+        for (Runnable callback : mOnPageScrollsInitializedCallbacks) {
+            callback.run();
+        }
+        mOnPageScrollsInitializedCallbacks.clear();
+    }
+
     @SuppressLint("DrawAllocation")
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         mIsLayoutValid = true;
         final int childCount = getChildCount();
+        int[] pageScrolls = mPageScrolls;
         boolean pageScrollChanged = false;
-        if (mPageScrolls == null || childCount != mPageScrolls.length) {
-            mPageScrolls = new int[childCount];
+        if (!pageScrollsInitialized()) {
+            pageScrolls = new int[childCount];
             pageScrollChanged = true;
         }
 
@@ -701,10 +727,8 @@
 
         if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
 
-        boolean isScrollChanged = getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC);
-        if (isScrollChanged) {
-            pageScrollChanged = true;
-        }
+        pageScrollChanged |= getPageScrolls(pageScrolls, true, SIMPLE_SCROLL_LOGIC);
+        mPageScrolls = pageScrolls;
 
         final LayoutTransition transition = getLayoutTransition();
         // If the transition is running defer updating max scroll, as some empty pages could
@@ -738,6 +762,7 @@
         if (mScroller.isFinished() && pageScrollChanged) {
             setCurrentPage(getNextPage());
         }
+        onPageScrollsInitialized();
     }
 
     /**
@@ -849,8 +874,10 @@
     @Override
     public void onViewRemoved(View child) {
         super.onViewRemoved(child);
-        mCurrentPage = validateNewPage(mCurrentPage);
-        mCurrentScrollOverPage = mCurrentPage;
+        runOnPageScrollsInitialized(() -> {
+            mCurrentPage = validateNewPage(mCurrentPage);
+            mCurrentScrollOverPage = mCurrentPage;
+        });
         dispatchPageCountChanged();
     }
 
@@ -1153,7 +1180,7 @@
     }
 
     public int getScrollForPage(int index) {
-        if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) {
+        if (!pageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
             return 0;
         } else {
             return mPageScrolls[index];
@@ -1163,7 +1190,7 @@
     // While layout transitions are occurring, a child's position may stray from its baseline
     // position. This method returns the magnitude of this stray at any given time.
     public int getLayoutTransitionOffsetForPage(int index) {
-        if (mPageScrolls == null || index >= mPageScrolls.length || index < 0) {
+        if (!pageScrollsInitialized() || index >= mPageScrolls.length || index < 0) {
             return 0;
         } else {
             View child = getChildAt(index);
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
index 4390b51..72ca5a7 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsContainerView.java
@@ -142,7 +142,7 @@
         mWorkManager = new WorkProfileManager(
                 mActivityContext.getSystemService(UserManager.class),
                 this,
-                Utilities.getPrefs(mActivityContext));
+                Utilities.getPrefs(mActivityContext), mActivityContext.getDeviceProfile());
         mAH = Arrays.asList(null, null);
         mAH.set(AdapterHolder.MAIN, new AdapterHolder(false /* isWork */));
         mAH.set(AdapterHolder.WORK, new AdapterHolder(true /* isWork */));
@@ -437,8 +437,12 @@
         if (showTabs == mUsingTabs && !force) {
             return;
         }
+
+        // replaceRVcontainer() needs to use both mUsingTabs value to remove the old view AND
+        // showTabs value to create new view. Hence the mUsingTabs new value assignment MUST happen
+        // after this call.
+        replaceRVContainer(showTabs);
         mUsingTabs = showTabs;
-        replaceRVContainer(mUsingTabs);
 
         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.MAIN).mRecyclerView);
         mAllAppsStore.unregisterIconContainer(mAH.get(AdapterHolder.WORK).mRecyclerView);
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index c5b02dd..dc9f18c 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -32,6 +32,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
@@ -73,15 +74,17 @@
     private final Predicate<ItemInfo> mMatcher;
 
     private WorkModeSwitch mWorkModeSwitch;
+    private final DeviceProfile mDeviceProfile;
 
     @WorkProfileState
     private int mCurrentState;
 
 
     public WorkProfileManager(UserManager userManager, BaseAllAppsContainerView<?> allApps,
-            SharedPreferences preferences) {
+            SharedPreferences preferences, DeviceProfile deviceProfile) {
         mUserManager = userManager;
         mAllApps = allApps;
+        mDeviceProfile = deviceProfile;
         mAdapterProvider = new WorkAdapterProvider(allApps.mActivityContext, preferences);
         mMatcher = mAllApps.mPersonalMatcher.negate();
     }
@@ -141,8 +144,11 @@
             mWorkModeSwitch = (WorkModeSwitch) mAllApps.getLayoutInflater().inflate(
                     R.layout.work_mode_fab, mAllApps, false);
         }
+        ViewGroup.MarginLayoutParams lp =
+                (ViewGroup.MarginLayoutParams) mWorkModeSwitch.getLayoutParams();
         int workFabMarginBottom =
-                mWorkModeSwitch.getResources().getDimensionPixelSize(R.dimen.work_fab_margin);
+                mWorkModeSwitch.getResources().getDimensionPixelSize(
+                        R.dimen.work_fab_margin_bottom);
         if (FeatureFlags.ENABLE_FLOATING_SEARCH_BAR.get()) {
             workFabMarginBottom <<= 1;  // Double margin to add space above search bar.
             workFabMarginBottom +=
@@ -151,8 +157,11 @@
         if (!mAllApps.mActivityContext.getDeviceProfile().isGestureMode){
             workFabMarginBottom += mAllApps.mActivityContext.getDeviceProfile().getInsets().bottom;
         }
-        ((ViewGroup.MarginLayoutParams) mWorkModeSwitch.getLayoutParams()).bottomMargin =
-                workFabMarginBottom;
+        lp.bottomMargin = workFabMarginBottom;
+        int totalScreenWidth = mDeviceProfile.widthPx;
+        int personalWorkTabWidth =
+                mAllApps.mActivityContext.getAppsView().getActiveRecyclerView().getTabWidth();
+        lp.rightMargin = lp.leftMargin = (totalScreenWidth - personalWorkTabWidth) / 2;
         if (mWorkModeSwitch.getParent() != mAllApps) {
             mAllApps.addView(mWorkModeSwitch);
         }
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 4ff5d5e..f8a2c79 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -156,14 +156,18 @@
                     String.format("upperBound (%f) must be greater than lowerBound (%f)",
                             upperBound, lowerBound));
         }
-        return t -> clampToProgress(t, lowerBound, upperBound);
+        return t -> clampToProgress(interpolator, t, lowerBound, upperBound);
     }
 
     /**
      * Returns the progress value's progress between the lower and upper bounds. That is, the
      * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound.
+     *
+     * Between lowerBound and upperBound, the progress value will be interpolated using the provided
+     * interpolator.
      */
-    public static float clampToProgress(float progress, float lowerBound, float upperBound) {
+    public static float clampToProgress(
+            Interpolator interpolator, float progress, float lowerBound, float upperBound) {
         if (upperBound < lowerBound) {
             throw new IllegalArgumentException(
                     String.format("upperBound (%f) must be greater than lowerBound (%f)",
@@ -179,7 +183,15 @@
         if (progress > upperBound) {
             return 1;
         }
-        return (progress - lowerBound) / (upperBound - lowerBound);
+        return interpolator.getInterpolation((progress - lowerBound) / (upperBound - lowerBound));
+    }
+
+    /**
+     * Returns the progress value's progress between the lower and upper bounds. That is, the
+     * progress will be 0f from 0f to lowerBound, and reach 1f by upperBound.
+     */
+    public static float clampToProgress(float progress, float lowerBound, float upperBound) {
+        return clampToProgress(Interpolators.LINEAR, progress, lowerBound, upperBound);
     }
 
     /**
diff --git a/src/com/android/launcher3/folder/FolderNameProvider.java b/src/com/android/launcher3/folder/FolderNameProvider.java
index 2b621bd..5021644 100644
--- a/src/com/android/launcher3/folder/FolderNameProvider.java
+++ b/src/com/android/launcher3/folder/FolderNameProvider.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.folder;
 
+import android.annotation.SuppressLint;
+import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.Process;
@@ -22,11 +24,15 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.model.AllAppsList;
 import com.android.launcher3.model.BaseModelUpdateTask;
 import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.StringCache;
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -94,6 +100,7 @@
     /**
      * Generate and rank the suggested Folder names.
      */
+    @WorkerThread
     public void getSuggestedFolderName(Context context,
             ArrayList<WorkspaceItemInfo> workspaceItemInfos,
             FolderNameInfos nameInfos) {
@@ -107,8 +114,7 @@
         Set<UserHandle> users = workspaceItemInfos.stream().map(w -> w.user)
                 .collect(Collectors.toSet());
         if (users.size() == 1 && !users.contains(Process.myUserHandle())) {
-            String workFolderName = context.getString(R.string.work_folder_name);
-            setAsLastSuggestion(nameInfos, workFolderName);
+            setAsLastSuggestion(nameInfos, getWorkFolderName(context));
         }
 
         // If all the icons are from same package (e.g., main icon, shortcut, shortcut)
@@ -130,6 +136,17 @@
         }
     }
 
+    @WorkerThread
+    @SuppressLint("NewApi")
+    private String getWorkFolderName(Context context) {
+        if (!Utilities.ATLEAST_T) {
+            return context.getString(R.string.work_folder_name);
+        }
+        return context.getSystemService(DevicePolicyManager.class).getResources()
+                .getString(StringCache.WORK_FOLDER_NAME, () ->
+                        context.getString(R.string.work_folder_name));
+    }
+
     private Optional<AppInfo> getAppInfoByPackageName(String packageName) {
         if (mAppInfos == null || mAppInfos.isEmpty()) {
             return Optional.empty();
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index a11bd4f..d5bcb0c 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -83,6 +83,7 @@
 import com.android.launcher3.uioverrides.PredictedAppIconInflater;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
@@ -129,7 +130,7 @@
             super(base, UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
                     LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                     CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE,
-                    WindowManagerProxy.INSTANCE);
+                    WindowManagerProxy.INSTANCE, DisplayController.INSTANCE);
             mIdp = idp;
             mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp);
             mObjectMap.put(LauncherAppState.INSTANCE,
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index e36d4cf..ef9250c 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -216,14 +216,15 @@
             return false;
         }
 
+        // Sort the items by the reading order.
+        Collections.sort(mHotseatDiff);
+        Collections.sort(mWorkspaceDiff);
+
         // Migrate hotseat
         HotseatPlacementSolution hotseatSolution = new HotseatPlacementSolution(mDb, mSrcReader,
                 mDestReader, mContext, mDestHotseatSize, mHotseatItems, mHotseatDiff);
         hotseatSolution.find();
 
-        // Sort the items by the reading order.
-        Collections.sort(mWorkspaceDiff);
-
         // Migrate workspace.
         // First we create a collection of the screens
         List<Integer> screens = new ArrayList<>();
diff --git a/src/com/android/launcher3/model/ReloadStringCacheTask.java b/src/com/android/launcher3/model/ReloadStringCacheTask.java
new file mode 100644
index 0000000..f4d4298
--- /dev/null
+++ b/src/com/android/launcher3/model/ReloadStringCacheTask.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model;
+
+import com.android.launcher3.LauncherAppState;
+
+/**
+ * Handles updates due to changes in Device Policy Management resources triggered by
+ * {@link android.app.admin.DevicePolicyManager#ACTION_DEVICE_POLICY_RESOURCE_UPDATED}.
+ */
+public class ReloadStringCacheTask extends BaseModelUpdateTask {
+    private ModelDelegate mModelDelegate;
+
+    public ReloadStringCacheTask(ModelDelegate modelDelegate) {
+        mModelDelegate = modelDelegate;
+    }
+
+    @Override
+    public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList appsList) {
+        synchronized (dataModel) {
+            mModelDelegate.loadStringCache(dataModel.stringCache);
+            StringCache cloneSC = dataModel.stringCache.clone();
+            scheduleCallbackTask(c -> c.bindStringCache(cloneSC));
+        }
+    }
+}
diff --git a/src/com/android/launcher3/model/StringCache.java b/src/com/android/launcher3/model/StringCache.java
index 663a463..9859ddc 100644
--- a/src/com/android/launcher3/model/StringCache.java
+++ b/src/com/android/launcher3/model/StringCache.java
@@ -35,6 +35,11 @@
     private static final String PREFIX = "Launcher.";
 
     /**
+     * Work folder name.
+     */
+    public static final String WORK_FOLDER_NAME = PREFIX + "WORK_FOLDER_NAME";
+
+    /**
      * User on-boarding title for work profile apps.
      */
     private static final String WORK_PROFILE_EDU = PREFIX + "WORK_PROFILE_EDU";
@@ -91,11 +96,6 @@
             PREFIX + "ALL_APPS_PERSONAL_TAB_ACCESSIBILITY";
 
     /**
-     * Work folder name.
-     */
-    private static final String WORK_FOLDER_NAME = PREFIX + "WORK_FOLDER_NAME";
-
-    /**
      * Label on widget tab to indicate work app widgets.
      */
     private static final String WIDGETS_WORK_TAB = PREFIX + "WIDGETS_WORK_TAB";
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index a6b481a..242d2d4 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -198,6 +198,12 @@
                     return new Point(cellRect.centerX(), cellRect.centerY());
                 });
 
+            case TestProtocol.REQUEST_HAS_TIS: {
+                response.putBoolean(
+                        TestProtocol.REQUEST_HAS_TIS, false);
+                return response;
+            }
+
             default:
                 return null;
         }
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index e8fd2ff..3a030a8 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -110,6 +110,7 @@
     public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT =
             "get-activities-created-count";
     public static final String REQUEST_GET_ACTIVITIES = "get-activities";
+    public static final String REQUEST_HAS_TIS = "has-touch-interaction-service";
 
     public static final String REQUEST_WORKSPACE_CELL_LAYOUT_SIZE = "workspace-cell-layout-size";
     public static final String REQUEST_WORKSPACE_CELL_CENTER = "workspace-cell-center";
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index a125fbe..09b8228 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_ALLAPPS;
 import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME;
@@ -330,9 +331,6 @@
                         Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
         }
-        if (targetState != mStartState) {
-            logReachedState(targetState);
-        }
         mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState));
         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
         anim.setFloatValues(startProgress, endProgress);
@@ -361,6 +359,8 @@
         boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState);
         if (shouldGoToTargetState) {
             goToTargetState(targetState);
+        } else {
+            logReachedState(mToState);
         }
     }
 
@@ -368,13 +368,19 @@
         if (!mLauncher.isInState(targetState)) {
             // If we're already in the target state, don't jump to it at the end of the animation in
             // case the user started interacting with it before the animation finished.
-            mLauncher.getStateManager().goToState(targetState, false /* animated */);
+            mLauncher.getStateManager().goToState(targetState, false /* animated */,
+                    forEndCallback(() -> logReachedState(targetState)));
+        } else {
+            logReachedState(targetState);
         }
         mLauncher.getRootView().getSysUiScrim().createSysuiMultiplierAnim(
                 1f).setDuration(0).start();
     }
 
     private void logReachedState(LauncherState targetState) {
+        if (mStartState == targetState) {
+            return;
+        }
         // Transition complete. log the action
         mLauncher.getStatsLogManager().logger()
                 .withSrcState(mStartState.statsLogOrdinal)
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
index b0e2ec1..48df04f 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java
@@ -95,7 +95,7 @@
         mTitle = findViewById(R.id.app_title);
         mSubtitle = findViewById(R.id.app_subtitle);
         mExpandToggle = findViewById(R.id.toggle);
-        findViewById(R.id.app_container).setAccessibilityDelegate(new AccessibilityDelegate() {
+        setAccessibilityDelegate(new AccessibilityDelegate() {
 
             @Override
             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
new file mode 100644
index 0000000..5f92199
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+/**
+ * Operations on home screen qsb.
+ */
+public class HomeQsb {
+
+    private final LauncherInstrumentation mLauncher;
+
+    HomeQsb(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+        mLauncher.waitForLauncherObject("search_container_hotseat");
+    }
+
+    /**
+     * Show search result page from tapping qsb.
+     */
+    public SearchResultFromQsb showSearchResult() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to open search result page");
+             LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.clickLauncherObject(
+                    mLauncher.waitForLauncherObject("search_container_hotseat"));
+            try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
+                    "clicked qsb to open search result page")) {
+                return new SearchResultFromQsb(mLauncher);
+            }
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 45a0196..39cd4ff 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -33,7 +33,7 @@
 /**
  * Ancestor for AppIcon and AppMenuItem.
  */
-abstract class Launchable {
+public abstract class Launchable {
 
     protected static final int DEFAULT_DRAG_STEPS = 10;
 
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index afb4f8d..2b3583e 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1499,9 +1499,14 @@
                 0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
     }
 
+    private boolean hasTIS() {
+        return getTestInfo(TestProtocol.REQUEST_HAS_TIS).getBoolean(TestProtocol.REQUEST_HAS_TIS);
+    }
+
+
     public void sendPointer(long downTime, long currentTime, int action, Point point,
             GestureScope gestureScope) {
-        final boolean notLauncher3 = !isLauncher3();
+        final boolean hasTIS = hasTIS();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
                 if (gestureScope != GestureScope.OUTSIDE_WITH_PILFER
@@ -1509,12 +1514,12 @@
                         && gestureScope != GestureScope.OUTSIDE_WITH_KEYCODE) {
                     expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
                 }
-                if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+                if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) {
                     expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
                 }
                 break;
             case MotionEvent.ACTION_UP:
-                if (notLauncher3 && gestureScope != GestureScope.INSIDE
+                if (hasTIS && gestureScope != GestureScope.INSIDE
                         && gestureScope != GestureScope.INSIDE_TO_OUTSIDE_WITHOUT_PILFER
                         && (gestureScope == GestureScope.OUTSIDE_WITH_PILFER
                         || gestureScope == GestureScope.INSIDE_TO_OUTSIDE)) {
@@ -1528,7 +1533,7 @@
                                     || gestureScope == GestureScope.OUTSIDE_WITHOUT_PILFER
                                     ? EVENT_TOUCH_UP : EVENT_TOUCH_CANCEL);
                 }
-                if (notLauncher3 && getNavigationModel() != NavigationModel.THREE_BUTTON) {
+                if (hasTIS && getNavigationModel() != NavigationModel.THREE_BUTTON) {
                     expectEvent(TestProtocol.SEQUENCE_TIS,
                             gestureScope == GestureScope.INSIDE_TO_OUTSIDE_WITH_KEYCODE
                                     || gestureScope == GestureScope.OUTSIDE_WITH_KEYCODE
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
new file mode 100644
index 0000000..82652c7
--- /dev/null
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.tapl;
+
+import android.widget.TextView;
+
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiObject2;
+
+/**
+ * Operations on search result page opened from home screen qsb.
+ */
+public class SearchResultFromQsb {
+    // The input resource id in the search box.
+    private static final String INPUT_RES = "input";
+    private final LauncherInstrumentation mLauncher;
+
+    SearchResultFromQsb(LauncherInstrumentation launcher) {
+        mLauncher = launcher;
+        mLauncher.waitForLauncherObject("search_container_all_apps");
+    }
+
+    /** Set the input to the search input edit text and update search results. */
+    public void searchForInput(String input) {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to search for result with an input");
+             LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
+            mLauncher.waitForLauncherObject(INPUT_RES).setText(input);
+        }
+    }
+
+    /** Find the app from search results with app name. */
+    public Launchable findAppIcon(String appName) {
+        UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName));
+        return new AllAppsAppIcon(mLauncher, icon);
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index b0b42ba..954af3d 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -112,6 +112,18 @@
     }
 
     /**
+     * Returns the home qsb.
+     *
+     * The qsb must already be visible when calling this method.
+     */
+    public HomeQsb getQsb() {
+        try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                "want to get the home qsb")) {
+            return new HomeQsb(mLauncher);
+        }
+    }
+
+    /**
      * Returns an icon for the app, if currently visible.
      *
      * @param appName name of the app