Merge "[Launcher] Added comment to keep logging latency type in sync" into udc-qpr-dev
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index e45d9fd..8a3ffb5 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -53,5 +53,5 @@
     <string name="setup_wizard_pkg" translatable="false" />
 
     <!-- This is a float because it is converted to dp later in DeviceProfile -->
-    <item name="taskbar_icon_size" type="dimen" format="float">44</item>
+    <item name="taskbar_icon_size" type="dimen" format="float">48.4</item>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index a1c9f05..0541e61 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -449,6 +449,11 @@
         return mControllers.taskbarDragController;
     }
 
+    @Nullable
+    public BubbleControllers getBubbleControllers() {
+        return mControllers.bubbleControllers.orElse(null);
+    }
+
     @Override
     public ViewCache getViewCache() {
         return mViewCache;
@@ -641,8 +646,12 @@
         mControllers.taskbarForceVisibleImmersiveController.updateSysuiFlags(systemUiStateFlags);
         mControllers.voiceInteractionWindowController.setIsVoiceInteractionWindowVisible(
                 (systemUiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0, fromInit);
-
         mControllers.uiController.updateStateForSysuiFlags(systemUiStateFlags);
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            controllers.bubbleBarController.updateStateForSysuiFlags(systemUiStateFlags);
+            controllers.bubbleStashedHandleViewController.setIsHomeButtonDisabled(
+                    mControllers.navbarButtonsViewController.isHomeDisabled());
+        });
     }
 
     /**
@@ -738,7 +747,7 @@
             }
         }
         mWindowLayoutParams.height = height;
-        mControllers.taskbarInsetsController.onTaskbarWindowHeightOrInsetsChanged();
+        mControllers.taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
     }
 
@@ -996,10 +1005,19 @@
      * Called when we want to unstash taskbar when user performs swipes up gesture.
      */
     public void onSwipeToUnstashTaskbar() {
-        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
+        mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(/* stash= */ false);
         mControllers.taskbarEduTooltipController.hide();
     }
 
+    /**
+     * Called when we want to open bubblebar when user performs swipes up gesture.
+     */
+    public void onSwipeToOpenBubblebar() {
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            controllers.bubbleStashController.showBubbleBar(/* expandBubbles= */ true);
+        });
+    }
+
     /** Returns {@code true} if Taskbar All Apps is open. */
     public boolean isTaskbarAllAppsOpen() {
         return mControllers.taskbarAllAppsController.isOpen();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index a9a2ccf..d237c1f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -34,6 +34,7 @@
 /** Helps draw the taskbar background, made up of a rectangle plus two inverted rounded corners. */
 class TaskbarBackgroundRenderer(context: TaskbarActivityContext) {
 
+    private val isInSetup: Boolean = !context.isUserSetupComplete
     private val DARK_THEME_SHADOW_ALPHA = 51f
     private val LIGHT_THEME_SHADOW_ALPHA = 25f
 
@@ -137,7 +138,7 @@
             canvas.translate(0f, leftCornerRadius)
             canvas.translate(canvas.width - rightCornerRadius, -rightCornerRadius)
             canvas.drawPath(invertedRightCornerPath, paint)
-        } else {
+        } else if (!isInSetup) {
             // backgroundHeight is a value from [0...maxBackgroundHeight], so we can use it as a
             // proxy to figure out the animation progress of the stash/unstash animation.
             val progress = backgroundHeight / maxBackgroundHeight
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 4f9b1e4..07cd8ff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -56,13 +56,13 @@
     private val touchableRegion: Region = Region()
     private val insetsOwner: IBinder = Binder()
     private val deviceProfileChangeListener = { _: DeviceProfile ->
-        onTaskbarWindowHeightOrInsetsChanged()
+        onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
     }
     private val gestureNavSettingsObserver =
         GestureNavigationSettingsObserver(
             context.mainThreadHandler,
             context,
-            this::onTaskbarWindowHeightOrInsetsChanged
+            this::onTaskbarOrBubblebarWindowHeightOrInsetsChanged
         )
 
     // Initialized in init.
@@ -72,7 +72,7 @@
     fun init(controllers: TaskbarControllers) {
         this.controllers = controllers
         windowLayoutParams = context.windowLayoutParams
-        onTaskbarWindowHeightOrInsetsChanged()
+        onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
 
         context.addOnDeviceProfileChangeListener(deviceProfileChangeListener)
         gestureNavSettingsObserver.registerForCallingUser()
@@ -83,7 +83,7 @@
         gestureNavSettingsObserver.unregister()
     }
 
-    fun onTaskbarWindowHeightOrInsetsChanged() {
+    fun onTaskbarOrBubblebarWindowHeightOrInsetsChanged() {
         val tappableHeight = controllers.taskbarStashController.tappableHeightToReportToApps
         // We only report tappableElement height for unstashed, persistent taskbar,
         // which is also when we draw the rounded corners above taskbar.
@@ -121,13 +121,33 @@
                 )
         }
 
-        val touchableHeight = controllers.taskbarStashController.touchableHeight
-        touchableRegion.set(
-            0,
-            windowLayoutParams.height - touchableHeight,
-            context.deviceProfile.widthPx,
-            windowLayoutParams.height
-        )
+        val taskbarTouchableHeight = controllers.taskbarStashController.touchableHeight
+        val bubblesTouchableHeight =
+            if (controllers.bubbleControllers.isPresent)
+                controllers.bubbleControllers.get().bubbleStashController.touchableHeight
+            else 0
+        val touchableHeight = Math.max(taskbarTouchableHeight, bubblesTouchableHeight)
+
+        if (
+            controllers.bubbleControllers.isPresent &&
+                controllers.bubbleControllers.get().bubbleStashController.isBubblesShowingOnHome
+        ) {
+            val iconBounds =
+                controllers.bubbleControllers.get().bubbleBarViewController.bubbleBarBounds
+            touchableRegion.set(
+                iconBounds.left,
+                iconBounds.top,
+                iconBounds.right,
+                iconBounds.bottom
+            )
+        } else {
+            touchableRegion.set(
+                0,
+                windowLayoutParams.height - touchableHeight,
+                context.deviceProfile.widthPx,
+                windowLayoutParams.height
+            )
+        }
         val contentHeight = controllers.taskbarStashController.contentHeightToReportToApps
         val res = context.resources
         for (provider in windowLayoutParams.providedInsets) {
@@ -211,6 +231,9 @@
             context.dragLayer,
             insetsInfo.touchableRegion
         )
+        val bubbleBarVisible =
+            controllers.bubbleControllers.isPresent &&
+                controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
         var insetsIsTouchableRegion = true
         if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
             // Let touches pass through us.
@@ -231,7 +254,9 @@
             insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
             insetsIsTouchableRegion = false
         } else if (
-            controllers.taskbarViewController.areIconsVisible() || context.isNavBarKidsModeActive
+            controllers.taskbarViewController.areIconsVisible() ||
+                context.isNavBarKidsModeActive ||
+                bubbleBarVisible
         ) {
             // Taskbar has some touchable elements, take over the full taskbar area
             if (
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 008f5f6..122745c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -404,6 +404,14 @@
                     + ", mLauncherState: " + mLauncherState
                     + ", toAlignment: " + toAlignment);
         }
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            // Show the bubble bar when on launcher home or in overview.
+            boolean onHome = isInLauncher && mLauncherState == LauncherState.NORMAL;
+            boolean onOverview = mLauncherState == LauncherState.OVERVIEW;
+            controllers.bubbleStashController.setBubblesShowingOnHome(onHome);
+            controllers.bubbleStashController.setBubblesShowingOnOverview(onOverview);
+        });
+
         AnimatorSet animatorSet = new AnimatorSet();
 
         if (hasAnyFlag(changedFlags, FLAG_LAUNCHER_IN_STATE_TRANSITION)) {
@@ -474,7 +482,8 @@
                     public void onAnimationEnd(Animator animation) {
                         TaskbarStashController stashController =
                                 mControllers.taskbarStashController;
-                        stashController.updateAndAnimateTransientTaskbar(/* stash */ true);
+                        stashController.updateAndAnimateTransientTaskbar(
+                                /* stash */ true, /* bubblesShouldFollow */ true);
                     }
                 });
             } else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
index 8e1059b..6cf63a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -18,6 +18,8 @@
 import android.util.SparseArray;
 import android.view.View;
 
+import androidx.annotation.UiThread;
+
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.model.BgDataModel;
 import com.android.launcher3.model.BgDataModel.FixedContainerItems;
@@ -29,6 +31,8 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Preconditions;
 import com.android.quickstep.RecentsModel;
 
 import java.io.PrintWriter;
@@ -37,6 +41,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Predicate;
 
 /**
@@ -250,9 +255,12 @@
         mControllers.taskbarPopupController.setDeepShortcutMap(deepShortcutMapCopy);
     }
 
+    @UiThread
     @Override
-    public void bindAllApplications(AppInfo[] apps, int flags) {
-        mControllers.taskbarAllAppsController.setApps(apps, flags);
+    public void bindAllApplications(AppInfo[] apps, int flags,
+            Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
+        Preconditions.assertUIThread();
+        mControllers.taskbarAllAppsController.setApps(apps, flags, packageUserKeytoUidMap);
         mControllers.taskbarRecentAppsController.setApps(apps);
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
index 5ea00cf..1c250bf 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarScrimViewController.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.taskbar;
 
+import static com.android.launcher3.taskbar.bubbles.BubbleBarController.BUBBLE_BAR_ENABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 
@@ -23,6 +24,7 @@
 import android.view.animation.PathInterpolator;
 
 import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.SystemUiProxy;
 
 import java.io.PrintWriter;
@@ -63,6 +65,10 @@
      * Updates the scrim state based on the flags.
      */
     public void updateStateForSysuiFlags(int stateFlags, boolean skipAnim) {
+        if (BUBBLE_BAR_ENABLED && DisplayController.isTransientTaskbar(mActivity)) {
+            // These scrims aren't used if bubble bar & transient taskbar are active.
+            return;
+        }
         final boolean bubblesExpanded = (stateFlags & SYSUI_STATE_BUBBLES_EXPANDED) != 0;
         final boolean manageMenuExpanded =
                 (stateFlags & SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index eb4c136..8f5646a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -255,14 +255,15 @@
     private boolean mEnableBlockingTimeoutDuringTests = false;
 
     // Evaluate whether the handle should be stashed
+    private final IntPredicate mIsStashedPredicate = flags -> {
+        boolean inApp = hasAnyFlag(flags, FLAGS_IN_APP);
+        boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP);
+        boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE);
+        boolean forceStashed = hasAnyFlag(flags, FLAGS_FORCE_STASHED);
+        return (inApp && stashedInApp) || (!inApp && stashedLauncherState) || forceStashed;
+    };
     private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder(
-            flags -> {
-                boolean inApp = hasAnyFlag(flags, FLAGS_IN_APP);
-                boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP);
-                boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE);
-                boolean forceStashed = hasAnyFlag(flags, FLAGS_FORCE_STASHED);
-                return (inApp && stashedInApp) || (!inApp && stashedLauncherState) || forceStashed;
-            });
+            mIsStashedPredicate);
 
     private boolean mIsTaskbarSystemActionRegistered = false;
     private TaskbarSharedState mTaskbarSharedState;
@@ -501,9 +502,19 @@
     }
 
     /**
-     * Stash or unstashes the transient taskbar.
+     * Stash or unstashes the transient taskbar, using the default TASKBAR_STASH_DURATION.
+     * If bubble bar exists, it will match taskbars stashing behavior.
      */
     public void updateAndAnimateTransientTaskbar(boolean stash) {
+        updateAndAnimateTransientTaskbar(stash, /* shouldBubblesFollow= */ true);
+    }
+
+    /**
+     * Stash or unstashes the transient taskbar.
+     * @param stash whether transient taskbar should be stashed.
+     * @param shouldBubblesFollow whether bubbles should match taskbars behavior.
+     */
+    public void updateAndAnimateTransientTaskbar(boolean stash,  boolean shouldBubblesFollow) {
         if (!DisplayController.isTransientTaskbar(mActivity)) {
             return;
         }
@@ -519,6 +530,34 @@
             updateStateForFlag(FLAG_STASHED_IN_APP_AUTO, stash);
             applyState();
         }
+
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            if (shouldBubblesFollow) {
+                final boolean willStash = mIsStashedPredicate.test(mState);
+                if (willStash != controllers.bubbleStashController.isStashed()) {
+                    // Typically bubbles gets stashed / unstashed along with Taskbar, however, if
+                    // taskbar is becoming stashed because bubbles is being expanded, we don't want
+                    // to stash bubbles.
+                    if (willStash) {
+                        controllers.bubbleStashController.stashBubbleBar();
+                    } else {
+                        controllers.bubbleStashController.showBubbleBar(false /* expandBubbles */);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * Stashes transient taskbar after it has timed out.
+     */
+    private void updateAndAnimateTransientTaskbarForTimeout() {
+        // If bubbles are expanded we shouldn't stash them when taskbar is hidden
+        // for the timeout.
+        boolean bubbleBarExpanded = mControllers.bubbleControllers.isPresent()
+                && mControllers.bubbleControllers.get().bubbleBarViewController.isExpanded();
+        updateAndAnimateTransientTaskbar(/* stash= */ true,
+                /* shouldBubblesFollow= */ !bubbleBarExpanded);
     }
 
     /**
@@ -886,7 +925,7 @@
     private void onIsStashedChanged(boolean isStashed) {
         mControllers.runAfterInit(() -> {
             mControllers.stashedHandleViewController.onIsStashedChanged(isStashed);
-            mControllers.taskbarInsetsController.onTaskbarWindowHeightOrInsetsChanged();
+            mControllers.taskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         });
     }
 
@@ -1133,7 +1172,7 @@
         if (mControllers.taskbarAutohideSuspendController.isSuspended()) {
             return;
         }
-        updateAndAnimateTransientTaskbar(true);
+        updateAndAnimateTransientTaskbarForTimeout();
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
index ec93846..deaf024 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
@@ -108,7 +108,17 @@
         }
 
     override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
-        if (!enabled || controllers.taskbarStashController.isStashed) {
+        if (!enabled) {
+            return false
+        }
+        val bubbleControllers = controllers.bubbleControllers.orElse(null)
+        if (bubbleControllers != null && bubbleControllers.bubbleBarViewController.isExpanded) {
+            return false
+        }
+        if (
+            (bubbleControllers == null || bubbleControllers.bubbleStashController.isStashed) &&
+                controllers.taskbarStashController.isStashed
+        ) {
             return false
         }
 
@@ -122,7 +132,12 @@
                 return true
             }
         } else if (ev.action == MotionEvent.ACTION_DOWN) {
-            if (screenCoordinatesEv.y < gestureHeightYThreshold) {
+            val isDownOnBubbleBar =
+                (bubbleControllers != null &&
+                    bubbleControllers.bubbleBarViewController.isEventOverAnyItem(
+                        screenCoordinatesEv
+                    ))
+            if (!isDownOnBubbleBar && screenCoordinatesEv.y < gestureHeightYThreshold) {
                 controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
             }
         }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
index 2b4e67c..916b1e6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarTranslationController.java
@@ -92,6 +92,10 @@
         mControllers.stashedHandleViewController.setTranslationYForSwipe(transY);
         mControllers.taskbarViewController.setTranslationYForSwipe(transY);
         mControllers.taskbarDragLayerController.setTranslationYForSwipe(transY);
+        mControllers.bubbleControllers.ifPresent(controllers -> {
+            controllers.bubbleBarViewController.setTranslationYForSwipe(transY);
+            controllers.bubbleStashedHandleViewController.setTranslationYForSwipe(transY);
+        });
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index 459a658..e0a502b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -26,7 +26,9 @@
 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
 import com.android.launcher3.util.PackageUserKey;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Predicate;
 
 /**
@@ -53,6 +55,8 @@
     private boolean mDisallowGlobalDrag;
     private boolean mDisallowLongClick;
 
+    private Map<PackageUserKey, Integer> mPackageUserKeytoUidMap = Collections.emptyMap();
+
     /** Initialize the controller. */
     public void init(TaskbarControllers controllers, boolean allAppsVisible) {
         mControllers = controllers;
@@ -67,11 +71,12 @@
     }
 
     /** Updates the current {@link AppInfo} instances. */
-    public void setApps(AppInfo[] apps, int flags) {
+    public void setApps(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
         mApps = apps;
         mAppsModelFlags = flags;
+        mPackageUserKeytoUidMap = map;
         if (mAppsView != null) {
-            mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags);
+            mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags, mPackageUserKeytoUidMap);
         }
     }
 
@@ -136,7 +141,7 @@
 
         viewController.show(animate);
         mAppsView = overlayContext.getAppsView();
-        mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags);
+        mAppsView.getAppsStore().setApps(mApps, mAppsModelFlags, mPackageUserKeytoUidMap);
         mAppsView.getFloatingHeaderView()
                 .findFixedRowByType(PredictionRowView.class)
                 .setPredictedApps(mPredictedApps);
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 82494c6..3786189 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -28,6 +28,8 @@
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarInsetsController;
+import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.SystemUiProxy;
@@ -51,6 +53,8 @@
     // Initialized in init.
     private BubbleStashController mBubbleStashController;
     private BubbleBarController mBubbleBarController;
+    private TaskbarStashController mTaskbarStashController;
+    private TaskbarInsetsController mTaskbarInsetsController;
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
 
@@ -80,6 +84,8 @@
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleBarController = bubbleControllers.bubbleBarController;
+        mTaskbarStashController = controllers.taskbarStashController;
+        mTaskbarInsetsController = controllers.taskbarInsetsController;
 
         mActivity.addOnDeviceProfileChangeListener(dp ->
                 mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight
@@ -89,7 +95,9 @@
         mBubbleClickListener = v -> onBubbleClicked(v);
         mBubbleBarClickListener = v -> setExpanded(true);
         mBarView.setOnClickListener(mBubbleBarClickListener);
-        // TODO: when barView layout changes tell taskbarInsetsController the insets have changed.
+        mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
+                mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
+        );
     }
 
     private void onBubbleClicked(View v) {
@@ -283,7 +291,8 @@
                 } else {
                     Log.w(TAG, "trying to expand bubbles when there isn't one selected");
                 }
-                // TODO: Tell taskbar stash controller to stash without bubbles following
+                mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */,
+                        false /* shouldBubblesFollow */);
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 0ab53b0..b3c7d41 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.taskbar.StashedHandleViewController;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.taskbar.TaskbarInsetsController;
 import com.android.launcher3.taskbar.TaskbarStashController;
 import com.android.launcher3.util.MultiPropertyFactory;
 
@@ -50,6 +51,7 @@
 
     // Initialized in init.
     private TaskbarControllers mControllers;
+    private TaskbarInsetsController mTaskbarInsetsController;
     private BubbleBarViewController mBarViewController;
     private BubbleStashedHandleViewController mHandleViewController;
     private TaskbarStashController mTaskbarStashController;
@@ -77,6 +79,7 @@
 
     public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
         mControllers = controllers;
+        mTaskbarInsetsController = controllers.taskbarInsetsController;
         mBarViewController = bubbleControllers.bubbleBarViewController;
         mHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
         mTaskbarStashController = controllers.taskbarStashController;
@@ -271,7 +274,7 @@
     private void onIsStashedChanged() {
         mControllers.runAfterInit(() -> {
             mHandleViewController.onIsStashedChanged();
-            // TODO: when stash changes tell taskbarInsetsController the insets have changed.
+            mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
         });
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
new file mode 100644
index 0000000..d8aa235
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DejankBinderTracker.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright (C) 2019 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.uioverrides;
+
+import static android.os.IBinder.FLAG_ONEWAY;
+
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+/**
+ * A binder proxy transaction listener for tracking non-whitelisted binder calls.
+ */
+public class DejankBinderTracker implements Binder.ProxyTransactListener {
+    private static final String TAG = "DejankBinderTracker";
+
+    private static final Object sLock = new Object();
+    private static final HashSet<String> sWhitelistedFrameworkClasses = new HashSet<>();
+    static {
+        // Common IPCs that are ok to block the main thread.
+        sWhitelistedFrameworkClasses.add("android.view.IWindowSession");
+        sWhitelistedFrameworkClasses.add("android.os.IPowerManager");
+    }
+    private static boolean sTemporarilyIgnoreTracking = false;
+
+    // Used by the client to limit binder tracking to specific regions
+    private static boolean sTrackingAllowed = false;
+
+    private BiConsumer<String, Integer> mUnexpectedTransactionCallback;
+    private boolean mIsTracking = false;
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Runnable}.
+     */
+    @MainThread
+    public static void whitelistIpcs(Runnable runnable) {
+        sTemporarilyIgnoreTracking = true;
+        runnable.run();
+        sTemporarilyIgnoreTracking = false;
+    }
+
+    /**
+     * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
+     */
+    @MainThread
+    public static <T> T whitelistIpcs(Supplier<T> supplier) {
+        sTemporarilyIgnoreTracking = true;
+        T value = supplier.get();
+        sTemporarilyIgnoreTracking = false;
+        return value;
+    }
+
+    /**
+     * Enables binder tracking during a test.
+     */
+    @MainThread
+    public static void allowBinderTrackingInTests() {
+        sTrackingAllowed = true;
+    }
+
+    /**
+     * Disables binder tracking during a test.
+     */
+    @MainThread
+    public static void disallowBinderTrackingInTests() {
+        sTrackingAllowed = false;
+    }
+
+    public DejankBinderTracker(BiConsumer<String, Integer> unexpectedTransactionCallback) {
+        mUnexpectedTransactionCallback = unexpectedTransactionCallback;
+    }
+
+    @MainThread
+    public void startTracking() {
+        if (!Build.TYPE.toLowerCase(Locale.ROOT).contains("debug")
+                && !Build.TYPE.toLowerCase(Locale.ROOT).equals("eng")) {
+            Log.wtf(TAG, "Unexpected use of binder tracker in non-debug build", new Exception());
+            return;
+        }
+        if (mIsTracking) {
+            return;
+        }
+        mIsTracking = true;
+        Binder.setProxyTransactListener(this);
+    }
+
+    @MainThread
+    public void stopTracking() {
+        if (!mIsTracking) {
+            return;
+        }
+        mIsTracking = false;
+        Binder.setProxyTransactListener(null);
+    }
+
+    // Override the hidden Binder#onTransactStarted method
+    public synchronized Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
+        if (!mIsTracking
+                || !sTrackingAllowed
+                || sTemporarilyIgnoreTracking
+                || (flags & FLAG_ONEWAY) == FLAG_ONEWAY
+                || !isMainThread()) {
+            return null;
+        }
+
+        String descriptor;
+        try {
+            descriptor = binder.getInterfaceDescriptor();
+            if (sWhitelistedFrameworkClasses.contains(descriptor)) {
+                return null;
+            }
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            descriptor = binder.getClass().getSimpleName();
+        }
+
+        mUnexpectedTransactionCallback.accept(descriptor, transactionCode);
+        return null;
+    }
+
+    @Override
+    public Object onTransactStarted(IBinder binder, int transactionCode) {
+        // Do nothing
+        return null;
+    }
+
+    @Override
+    public void onTransactEnded(Object session) {
+        // Do nothing
+    }
+
+    public static boolean isMainThread() {
+        return Thread.currentThread() == Looper.getMainLooper().getThread();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index bf4896d..2b92188 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -112,7 +112,6 @@
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
 import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.VibratorWrapper;
 import com.android.launcher3.util.WindowBounds;
@@ -588,7 +587,7 @@
         if (mWasLauncherAlreadyVisible) {
             mStateCallback.setState(STATE_LAUNCHER_DRAWN);
         } else {
-            SafeCloseable traceToken = TraceHelper.INSTANCE.beginAsyncSection("WTS-init");
+            Object traceToken = TraceHelper.INSTANCE.beginSection("WTS-init");
             View dragLayer = activity.getDragLayer();
             dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() {
                 boolean mHandled = false;
@@ -600,7 +599,7 @@
                     }
                     mHandled = true;
 
-                    traceToken.close();
+                    TraceHelper.INSTANCE.endSection(traceToken);
                     dragLayer.post(() ->
                             dragLayer.getViewTreeObserver().removeOnDrawListener(this));
                     if (activity != mActivity) {
@@ -682,10 +681,11 @@
     private void initializeLauncherAnimationController() {
         buildAnimationController();
 
-        try (SafeCloseable c = TraceHelper.INSTANCE.allowIpcs("logToggleRecents")) {
-            LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
-                    (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
-        }
+        Object traceToken = TraceHelper.INSTANCE.beginSection("logToggleRecents",
+                TraceHelper.FLAG_IGNORE_BINDERS);
+        LatencyTracker.getInstance(mContext).logAction(LatencyTracker.ACTION_TOGGLE_RECENTS,
+                (int) (mLauncherFrameDrawnTime - mTouchTimeMs));
+        TraceHelper.INSTANCE.endSection(traceToken);
 
         // This method is only called when STATE_GESTURE_STARTED is set, so we can enable the
         // high-res thumbnail loader here once we are sure that we will end up in an overview state
@@ -2039,9 +2039,10 @@
 
     private void setScreenshotCapturedState() {
         // If we haven't posted a draw callback, set the state immediately.
-        TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(SCREENSHOT_CAPTURED_EVT,
+                TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS);
         mStateCallback.setStateOnUiThread(STATE_SCREENSHOT_CAPTURED);
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     private void finishCurrentTransitionToRecents() {
diff --git a/quickstep/src/com/android/quickstep/BinderTracker.java b/quickstep/src/com/android/quickstep/BinderTracker.java
deleted file mode 100644
index a876cd8..0000000
--- a/quickstep/src/com/android/quickstep/BinderTracker.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/**
- * Copyright (C) 2023 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.quickstep;
-
-import static android.os.IBinder.FLAG_ONEWAY;
-
-import android.os.Binder;
-import android.os.Binder.ProxyTransactListener;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.os.Trace;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.util.SafeCloseable;
-import com.android.launcher3.util.TraceHelper;
-
-import java.util.LinkedList;
-import java.util.Set;
-import java.util.function.Consumer;
-
-import kotlin.random.Random;
-
-/**
- * A binder proxy transaction listener for tracking binder calls on main thread.
- */
-public class BinderTracker {
-
-    private static final String TAG = "BinderTracker";
-
-    // Common IPCs that are ok to block the main thread.
-    private static final Set<String> sAllowedFrameworkClasses = Set.of(
-            "android.view.IWindowSession",
-            "android.os.IPowerManager",
-            "android.os.IServiceManager");
-
-    /**
-     * Starts tracking binder class and returns a {@link SafeCloseable} to end tracking
-     */
-    public static SafeCloseable startTracking(Consumer<BinderCallSite> callback) {
-        TraceHelper current = TraceHelper.INSTANCE;
-
-        TraceHelperExtension helper = new TraceHelperExtension(callback);
-        TraceHelper.INSTANCE = helper;
-        Binder.setProxyTransactListener(helper);
-
-        return () -> {
-            Binder.setProxyTransactListener(null);
-            TraceHelper.INSTANCE = current;
-        };
-    }
-
-    private static final LinkedList<String> mMainThreadTraceStack = new LinkedList<>();
-    private static final LinkedList<String> mMainThreadIgnoreIpcStack = new LinkedList<>();
-
-    private static class TraceHelperExtension extends TraceHelper implements ProxyTransactListener {
-
-        private final Consumer<BinderCallSite> mUnexpectedTransactionCallback;
-
-        TraceHelperExtension(Consumer<BinderCallSite> unexpectedTransactionCallback) {
-            mUnexpectedTransactionCallback = unexpectedTransactionCallback;
-        }
-
-        @Override
-        public void beginSection(String sectionName) {
-            if (isMainThread()) {
-                mMainThreadTraceStack.add(sectionName);
-            }
-            super.beginSection(sectionName);
-        }
-
-        @Override
-        public SafeCloseable beginAsyncSection(String sectionName) {
-            if (!isMainThread()) {
-                return super.beginAsyncSection(sectionName);
-            }
-
-            mMainThreadTraceStack.add(sectionName);
-            int cookie = Random.Default.nextInt();
-            Trace.beginAsyncSection(sectionName, cookie);
-            return () -> {
-                Trace.endAsyncSection(sectionName, cookie);
-                mMainThreadTraceStack.remove(sectionName);
-            };
-        }
-
-        @Override
-        public void endSection() {
-            super.endSection();
-            if (isMainThread()) {
-                mMainThreadTraceStack.pollLast();
-            }
-        }
-
-        @Override
-        public SafeCloseable allowIpcs(String rpcName) {
-            if (!isMainThread()) {
-                return super.allowIpcs(rpcName);
-            }
-
-            mMainThreadTraceStack.add(rpcName);
-            mMainThreadIgnoreIpcStack.add(rpcName);
-            int cookie = Random.Default.nextInt();
-            Trace.beginAsyncSection(rpcName, cookie);
-            return () -> {
-                Trace.endAsyncSection(rpcName, cookie);
-                mMainThreadTraceStack.remove(rpcName);
-                mMainThreadIgnoreIpcStack.remove(rpcName);
-            };
-        }
-
-        @Override
-        public Object onTransactStarted(IBinder binder, int transactionCode, int flags) {
-            if (!isMainThread() || (flags & FLAG_ONEWAY) == FLAG_ONEWAY) {
-                return null;
-            }
-
-            String ipcBypass = mMainThreadIgnoreIpcStack.peekLast();
-            String descriptor;
-            try {
-                descriptor = binder.getInterfaceDescriptor();
-                if (sAllowedFrameworkClasses.contains(descriptor)) {
-                    return null;
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "Error getting IPC descriptor", e);
-                descriptor = binder.getClass().getSimpleName();
-            }
-
-            if (ipcBypass == null) {
-                mUnexpectedTransactionCallback.accept(new BinderCallSite(
-                        mMainThreadTraceStack.peekLast(), descriptor, transactionCode));
-            } else {
-                Log.d(TAG, "MainThread-IPC " + descriptor + " ignored due to " + ipcBypass);
-            }
-            return null;
-        }
-
-        @Override
-        public Object onTransactStarted(IBinder binder, int transactionCode) {
-            // Do nothing
-            return null;
-        }
-
-        @Override
-        public void onTransactEnded(Object session) {
-            // Do nothing
-        }
-    }
-
-    private static boolean isMainThread() {
-        return Thread.currentThread() == Looper.getMainLooper().getThread();
-    }
-
-    /**
-     * Information about a binder call
-     */
-    public static class BinderCallSite {
-
-        @Nullable
-        public final String activeTrace;
-        public final String descriptor;
-        public final int transactionCode;
-
-        BinderCallSite(String activeTrace, String descriptor, int transactionCode) {
-            this.activeTrace = activeTrace;
-            this.descriptor = descriptor;
-            this.transactionCode = transactionCode;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
index 529213c..7638541 100644
--- a/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
+++ b/quickstep/src/com/android/quickstep/InstantAppResolverImpl.java
@@ -16,13 +16,10 @@
 
 package com.android.quickstep;
 
-import android.app.ActivityThread;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.os.RemoteException;
-import android.util.Log;
 
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.util.InstantAppResolver;
@@ -52,14 +49,4 @@
         ComponentName cn = info.getTargetComponent();
         return cn != null && cn.getClassName().equals(COMPONENT_CLASS_MARKER);
     }
-
-    @Override
-    public boolean isInstantApp(String packageName, int userId) {
-        try {
-            return ActivityThread.getPackageManager().isInstantApp(packageName, userId);
-        } catch (RemoteException e) {
-            Log.e(TAG, "Failed to determine whether package is instant app " + packageName, e);
-            return false;
-        }
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
index 128b045..5f589bf 100644
--- a/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
+++ b/quickstep/src/com/android/quickstep/QuickstepProcessInitializer.java
@@ -60,10 +60,5 @@
         // Elevate GPU priority for Quickstep and Remote animations.
         ThreadedRenderer.setContextPriority(
                 ThreadedRenderer.EGL_CONTEXT_PRIORITY_HIGH_IMG);
-
-        if (BuildConfig.IS_STUDIO_BUILD) {
-            BinderTracker.startTracking(call ->  Log.e("BinderCall",
-                    call.descriptor + " called on mainthread under " + call.activeTrace));
-        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index 211aeb9..031d409 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -79,13 +79,6 @@
                 return response;
             }
 
-            case TestProtocol.REQUEST_GET_OVERVIEW_TASK_BORDER_WIDTH: {
-                Resources res = mContext.getResources();
-                response.putInt(TestProtocol.TEST_INFO_RESPONSE_FIELD,
-                        res.getDimensionPixelSize(R.dimen.keyboard_quick_switch_border_width));
-                return response;
-            }
-
             case TestProtocol.REQUEST_HAS_TIS: {
                 response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, true);
                 return response;
diff --git a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
index 810c028..8135238 100644
--- a/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskShortcutFactory.java
@@ -393,12 +393,11 @@
         @Override
         public List<SystemShortcut> getShortcuts(BaseDraggingActivity activity,
                 TaskIdAttributeContainer taskContainer) {
-            Task t = taskContainer.getTask();
-            return InstantAppResolver.newInstance(activity).isInstantApp(
-                    t.getTopComponent().getPackageName(), t.getKey().userId)
-                    ? Collections.singletonList(new SystemShortcut.Install(activity,
-                            taskContainer.getItemInfo(), taskContainer.getTaskView()))
-                    : null;
+            return InstantAppResolver.newInstance(activity).isInstantApp(activity,
+                    taskContainer.getTask().getTopComponent().getPackageName()) ?
+                    Collections.singletonList(new SystemShortcut.Install(activity,
+                            taskContainer.getItemInfo(), taskContainer.getTaskView())) :
+                    null;
         }
     };
 
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
index 80a449b..67360c4 100644
--- a/quickstep/src/com/android/quickstep/TaskUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -33,7 +33,6 @@
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageManagerHelper;
-import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
@@ -52,8 +51,7 @@
      * TODO: remove this once we switch to getting the icon and label from IconCache.
      */
     public static CharSequence getTitle(Context context, Task task) {
-        return TraceHelper.allowIpcs("TaskUtils.getTitle", () ->
-                getTitle(context, task.key.userId, task.getTopComponent().getPackageName()));
+        return getTitle(context, task.key.userId, task.getTopComponent().getPackageName());
     }
 
     public static CharSequence getTitle(
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index af57172..2a1cc70 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -102,7 +102,6 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.OnboardingPrefs;
-import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.TraceHelper;
 import com.android.quickstep.inputconsumers.AccessibilityInputConsumer;
 import com.android.quickstep.inputconsumers.AssistantInputConsumer;
@@ -701,7 +700,8 @@
             return;
         }
 
-        SafeCloseable traceToken = TraceHelper.INSTANCE.allowIpcs("TIS.onInputEvent");
+        Object traceToken = TraceHelper.INSTANCE.beginFlagsOverride(
+                TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
 
         final int action = event.getActionMasked();
         // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
@@ -797,7 +797,7 @@
         if (cleanUpConsumer) {
             reset();
         }
-        traceToken.close();
+        TraceHelper.INSTANCE.endFlagsOverride(traceToken);
         ProtoTracer.INSTANCE.get(this).scheduleFrameUpdate();
     }
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 10c6316..5b27f9b 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -28,6 +28,7 @@
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
 import static com.android.launcher3.util.VelocityUtils.PX_PER_MS;
 import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
 
@@ -228,7 +229,8 @@
                 // Until we detect the gesture, handle events as we receive them
                 mInputEventReceiver.setBatchingEnabled(false);
 
-                TraceHelper.INSTANCE.beginSection(DOWN_EVT);
+                Object traceToken = TraceHelper.INSTANCE.beginSection(DOWN_EVT,
+                        FLAG_CHECK_FOR_RACE_CONDITIONS);
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
@@ -239,7 +241,7 @@
                     startTouchTrackingForWindowAnimation(ev.getEventTime());
                 }
 
-                TraceHelper.INSTANCE.endSection();
+                TraceHelper.INSTANCE.endSection(traceToken);
                 break;
             }
             case ACTION_POINTER_DOWN: {
@@ -415,7 +417,8 @@
      * the animation can still be running.
      */
     private void finishTouchTracking(MotionEvent ev) {
-        TraceHelper.INSTANCE.beginSection(UP_EVT);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(UP_EVT,
+                FLAG_CHECK_FOR_RACE_CONDITIONS);
 
         if (mPassedWindowMoveSlop && mInteractionHandler != null) {
             if (ev.getActionMasked() == ACTION_CANCEL) {
@@ -452,7 +455,7 @@
             onInteractionGestureFinished();
         }
         cleanupAfterGesture();
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     private void cleanupAfterGesture() {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index fbe7fde..e9a0761 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
 import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.util.DisplayController;
 import com.android.quickstep.InputConsumer;
@@ -62,6 +63,7 @@
     private final int mTaskbarNavThresholdY;
     private final boolean mIsTaskbarAllAppsOpen;
     private boolean mHasPassedTaskbarNavThreshold;
+    private boolean mIsInBubbleBarArea;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -136,7 +138,7 @@
                         mHasPassedTaskbarNavThreshold = false;
                         mTaskbarActivityContext.setAutohideSuspendFlag(
                                 FLAG_AUTOHIDE_SUSPEND_TOUCHING, true);
-                        if (isInArea(x)) {
+                        if (isInTaskbarArea(x)) {
                             if (!mIsTransientTaskbar) {
                                 mLongPressDownX = x;
                                 mLongPressDownY = y;
@@ -145,10 +147,12 @@
                                 mCanceledUnstashHint = false;
                             }
                         }
-
                         if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) {
                             mTransitionCallback.onActionDown();
                         }
+                        if (mIsTransientTaskbar && isInBubbleBarArea(x)) {
+                            mIsInBubbleBarArea = true;
+                        }
                         break;
                     case MotionEvent.ACTION_POINTER_UP:
                         int ptrIdx = ev.getActionIndex();
@@ -185,7 +189,11 @@
 
                             if (!mHasPassedTaskbarNavThreshold && passedTaskbarNavThreshold) {
                                 mHasPassedTaskbarNavThreshold = true;
-                                mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                                if (mIsInBubbleBarArea) {
+                                    mTaskbarActivityContext.onSwipeToOpenBubblebar();
+                                } else {
+                                    mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+                                }
                             }
 
                             if (dY < 0) {
@@ -208,21 +216,32 @@
                             mTransitionCallback.onActionEnd();
                         }
                         mHasPassedTaskbarNavThreshold = false;
+                        mIsInBubbleBarArea = false;
                         break;
                 }
             }
         }
     }
 
-    private boolean isInArea(float x) {
+    private boolean isInTaskbarArea(float x) {
         float areaFromMiddle = mUnstashArea / 2.0f;
         float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
         return distFromMiddle < areaFromMiddle;
     }
 
+    private boolean isInBubbleBarArea(float x) {
+        if (mTaskbarActivityContext != null && mIsTransientTaskbar) {
+            BubbleControllers controllers = mTaskbarActivityContext.getBubbleControllers();
+            if (controllers == null) return false;
+            Rect bubbleBarBounds = controllers.bubbleBarViewController.getBubbleBarBounds();
+            return x >= bubbleBarBounds.left && x <= bubbleBarBounds.right;
+        }
+        return false;
+    }
+
     private void onLongPressDetected(MotionEvent motionEvent) {
         if (mTaskbarActivityContext != null
-                && isInArea(motionEvent.getRawX())
+                && isInTaskbarArea(motionEvent.getRawX())
                 && !mIsTransientTaskbar) {
             boolean taskBarPressed = mTaskbarActivityContext.onLongPressToUnstashTaskbar();
             if (taskBarPressed) {
diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
index 084f8c1..5ec92a6 100644
--- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java
@@ -412,10 +412,12 @@
         }
 
         mFeedbackTitleView.setText(titleResId);
-        mFeedbackSubtitleView.setText(spokenSubtitleResId == NO_ID
-                ? mContext.getText(subtitleResId)
-                : Utilities.wrapForTts(
-                        mContext.getText(subtitleResId), mContext.getString(spokenSubtitleResId)));
+        mFeedbackSubtitleView.setText(
+                ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID
+                        ? mContext.getText(subtitleResId)
+                        : Utilities.wrapForTts(
+                                mContext.getText(subtitleResId),
+                                mContext.getString(spokenSubtitleResId)));
         if (isGestureSuccessful) {
             if (mTutorialFragment.isAtFinalStep()) {
                 showActionButton();
diff --git a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
index fbb8109..6288937 100644
--- a/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
+++ b/quickstep/src/com/android/quickstep/logging/StatsLogCompatManager.java
@@ -566,9 +566,10 @@
      * Helps to construct and log statsd compatible impression events.
      */
     private static class StatsCompatImpressionLogger implements StatsImpressionLogger {
-        private final IntArray mResultTypeList = new IntArray();
-        private final IntArray mResultCountList = new IntArray();
+        private int[] mResultTypeList = new int[]{};
+        private int[] mResultCountList = new int[]{};
         private final List<Boolean> mAboveKeyboardList = new ArrayList<>();
+        private int[] mUidList = new int[]{};
         private InstanceId mInstanceId = DEFAULT_INSTANCE_ID;
         private State mLauncherState = State.UNKNOWN;
         private int mQueryLength = -1;
@@ -593,28 +594,32 @@
 
         @Override
         public StatsImpressionLogger withResultType(IntArray resultType) {
-            this.mResultTypeList.clear();
-            this.mResultTypeList.addAll(resultType);
+            mResultTypeList = resultType.toArray();
             return this;
         }
 
         @Override
         public StatsImpressionLogger withResultCount(IntArray resultCount) {
-            this.mResultCountList.clear();
-            this.mResultCountList.addAll(resultCount);
+            mResultCountList = resultCount.toArray();
             return this;
         }
 
         @Override
         public StatsImpressionLogger withAboveKeyboard(List<Boolean> aboveKeyboard) {
-            this.mAboveKeyboardList.clear();
+            mAboveKeyboardList.clear();
             this.mAboveKeyboardList.addAll(aboveKeyboard);
             return this;
         }
 
         @Override
+        public StatsImpressionLogger withUids(IntArray uid) {
+            mUidList = uid.toArray();
+            return this;
+        }
+
+        @Override
         public void log(EventEnum event) {
-            boolean [] mAboveKeyboard = new boolean[mAboveKeyboardList.size()];
+            boolean[] mAboveKeyboard = new boolean[mAboveKeyboardList.size()];
             for (int i = 0; i < mAboveKeyboardList.size(); i++) {
                 mAboveKeyboard[i] = mAboveKeyboardList.get(i);
             }
@@ -626,11 +631,12 @@
                 logStringBuilder.append(String.format("ImpressionEvent:%s ", name));
                 logStringBuilder.append(String.format("LauncherState = %s ", mLauncherState));
                 logStringBuilder.append(String.format("QueryLength = %s ", mQueryLength));
-                for (int i = 0; i < mResultTypeList.size(); i++) {
+                for (int i = 0; i < mResultTypeList.length; i++) {
                     logStringBuilder.append(String.format(
-                            "\n ResultType = %s with ResultCount = %s with is_above_keyboard = %s",
-                            mResultTypeList.get(i), mResultCountList.get(i),
-                            mAboveKeyboard[i]));
+                            "\n ResultType = %s with ResultCount = %s with is_above_keyboard = %s"
+                                    + " with uid = %s",
+                            mResultTypeList[i], mResultCountList[i],
+                            mAboveKeyboard[i], mUidList[i]));
                 }
                 Log.d(IMPRESSION_TAG, logStringBuilder.toString());
             }
@@ -643,11 +649,13 @@
                     mLauncherState.getLauncherState(), // state
                     mQueryLength, // query_length
                     //result type list
-                    mResultTypeList.toArray(),
+                    mResultTypeList,
                     // result count list
-                    mResultCountList.toArray(),
+                    mResultCountList,
                     // above keyboard list
-                    mAboveKeyboard
+                    mAboveKeyboard,
+                    // uid list
+                    mUidList
             );
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/BinderTracker.java b/quickstep/src/com/android/quickstep/util/BinderTracker.java
new file mode 100644
index 0000000..cb04e5b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/BinderTracker.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 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.quickstep.util;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.launcher3.config.FeatureFlags;
+
+/**
+ * Utility class to test and check binder calls during development.
+ */
+public class BinderTracker {
+
+    private static final String TAG = "BinderTracker";
+
+    public static void start() {
+        if (!FeatureFlags.IS_STUDIO_BUILD) {
+            Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
+            return;
+        }
+
+        Binder.setProxyTransactListener(new Tracker());
+    }
+
+    public static void stop() {
+        if (!FeatureFlags.IS_STUDIO_BUILD) {
+            Log.wtf(TAG, "Accessing tracker in released code.", new Exception());
+            return;
+        }
+        Binder.setProxyTransactListener(null);
+    }
+
+    private static class Tracker implements Binder.ProxyTransactListener {
+
+        @Override
+        public Object onTransactStarted(IBinder iBinder, int code) {
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                Log.e(TAG, "Binder call on ui thread", new Exception());
+            }
+            return null;
+        }
+
+        @Override
+        public void onTransactEnded(Object session) { }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.java b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
index f252949..7563187 100644
--- a/quickstep/src/com/android/quickstep/util/BorderAnimator.java
+++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.java
@@ -28,7 +28,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Px;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.anim.AnimatedFloat;
@@ -176,12 +175,6 @@
         }
     }
 
-    @NonNull
-    @VisibleForTesting
-    public AnimatedFloat getBorderAnimationProgress() {
-        return mBorderAnimationProgress;
-    }
-
     /**
      * Callback to update the border bounds when building this animation.
      */
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
index 8e6415b..0631537 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt
@@ -62,7 +62,8 @@
     companion object {
         @IntDef(SPLIT_TASK_TASK, SPLIT_TASK_PENDINGINTENT, SPLIT_TASK_SHORTCUT,
                 SPLIT_PENDINGINTENT_TASK, SPLIT_PENDINGINTENT_PENDINGINTENT, SPLIT_SHORTCUT_TASK,
-                SPLIT_SINGLE_TASK_FULLSCREEN, SPLIT_SINGLE_INTENT_FULLSCREEN)
+                SPLIT_SINGLE_TASK_FULLSCREEN, SPLIT_SINGLE_INTENT_FULLSCREEN,
+                SPLIT_SINGLE_SHORTCUT_FULLSCREEN)
         @Retention(AnnotationRetention.SOURCE)
         annotation class SplitLaunchType
 
@@ -76,6 +77,7 @@
         // Non-split edge case of launching the initial selected task as a fullscreen task
         const val SPLIT_SINGLE_TASK_FULLSCREEN = 6
         const val SPLIT_SINGLE_INTENT_FULLSCREEN = 7
+        const val SPLIT_SINGLE_SHORTCUT_FULLSCREEN = 8
     }
 
 
@@ -217,8 +219,7 @@
     fun getFullscreenLaunchData() : SplitLaunchData {
         // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
         convertIntentsToFinalTypes()
-        val splitLaunchType = if (initialTaskId != INVALID_TASK_ID) SPLIT_SINGLE_TASK_FULLSCREEN
-        else SPLIT_SINGLE_INTENT_FULLSCREEN
+        val splitLaunchType = getFullscreenLaunchType()
 
         return generateSplitLaunchData(splitLaunchType)
     }
@@ -309,6 +310,22 @@
         throw IllegalStateException("Unidentified split launch type")
     }
 
+    @SplitLaunchType
+    private fun getFullscreenLaunchType(): Int {
+        if (initialTaskId != INVALID_TASK_ID) {
+            return SPLIT_SINGLE_TASK_FULLSCREEN
+        }
+
+        if (initialShortcut != null) {
+            return SPLIT_SINGLE_SHORTCUT_FULLSCREEN
+        }
+
+        if (initialIntent != null) {
+            return SPLIT_SINGLE_INTENT_FULLSCREEN
+        }
+        throw IllegalStateException("Unidentified fullscreen launch type")
+    }
+
     data class SplitLaunchData(
             @SplitLaunchType
             val splitLaunchType: Int,
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index ec8be89..45df831 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -28,6 +28,7 @@
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_INTENT_FULLSCREEN;
+import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_SHORTCUT_FULLSCREEN;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK_FULLSCREEN;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT;
 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
@@ -524,6 +525,7 @@
         PendingIntent firstPI = launchData.getInitialPendingIntent();
         int firstUserId = launchData.getInitialUserId();
         int initialStagePosition = launchData.getInitialStagePosition();
+        ShortcutInfo initialShortcut = launchData.getInitialShortcut();
         Bundle optionsBundle = options1.toBundle();
 
         final RemoteSplitLaunchTransitionRunner animationRunner =
@@ -531,17 +533,19 @@
         final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
                 ActivityThread.currentActivityThread().getApplicationThread(),
                 "LaunchSplitPair");
-        Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds =
-                LogUtils.getShellShareableInstanceId();
+        InstanceId instanceId = LogUtils.getShellShareableInstanceId().first;
         if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
                         optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
-                        DEFAULT_SPLIT_RATIO, remoteTransition, instanceIds.first);
+                        DEFAULT_SPLIT_RATIO, remoteTransition, instanceId);
                 case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
                         firstUserId, optionsBundle, secondTaskId, null /*options2*/,
                         initialStagePosition, DEFAULT_SPLIT_RATIO, remoteTransition,
-                        instanceIds.first);
+                        instanceId);
+                case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
+                        initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
+                        initialStagePosition, DEFAULT_SPLIT_RATIO, remoteTransition, instanceId);
             }
         } else {
             final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId,
@@ -549,12 +553,16 @@
             switch (launchData.getSplitLaunchType()) {
                 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition(
                         firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
-                        initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, instanceIds.first);
+                        initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, instanceId);
                 case SPLIT_SINGLE_INTENT_FULLSCREEN ->
                         mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId,
                                 optionsBundle, secondTaskId, null /*options2*/,
                                 initialStagePosition, DEFAULT_SPLIT_RATIO, adapter,
-                                instanceIds.first);
+                                instanceId);
+                case SPLIT_SINGLE_SHORTCUT_FULLSCREEN ->
+                        mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(
+                                initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
+                                initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, instanceId);
             }
         }
     }
diff --git a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
index c82cdb7..5f7d694 100644
--- a/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/SystemWindowManagerProxy.java
@@ -24,6 +24,7 @@
 import android.view.WindowMetrics;
 
 import com.android.internal.policy.SystemBarUtils;
+import com.android.launcher3.logging.FileLog;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.CachedDisplayInfo;
 import com.android.launcher3.util.window.WindowManagerProxy;
@@ -60,6 +61,7 @@
         WindowManager windowManager = displayInfoContext.getSystemService(WindowManager.class);
         Set<WindowMetrics> possibleMaximumWindowMetrics =
                 windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
+        FileLog.d("b/283944974", "possibleMaximumWindowMetrics: " + possibleMaximumWindowMetrics);
         for (WindowMetrics windowMetrics : possibleMaximumWindowMetrics) {
             CachedDisplayInfo info = getDisplayInfo(windowMetrics, Surface.ROTATION_0);
             List<WindowBounds> bounds = estimateWindowBounds(displayInfoContext, info);
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 7f035a2..f8893bd 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -112,7 +112,8 @@
         mContext = context;
         mSizeStrategy = sizeStrategy;
 
-        mOrientationState = TraceHelper.allowIpcs("TaskViewSimulator.init",
+        // TODO(b/187074722): Don't create this per-TaskViewSimulator
+        mOrientationState = TraceHelper.allowIpcs("",
                 () -> new RecentsOrientedState(context, sizeStrategy, i -> { }));
         mOrientationState.setGestureActive(true);
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 839c3fd..200252a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -32,7 +32,6 @@
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
 import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition;
-import static com.android.quickstep.TaskOverlayFactory.getEnabledShortcuts;
 import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR;
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
@@ -71,7 +70,6 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
 
 import com.android.app.animation.Interpolators;
 import com.android.launcher3.DeviceProfile;
@@ -90,7 +88,6 @@
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
-import com.android.launcher3.util.TraceHelper;
 import com.android.launcher3.util.TransformingTouchDelegate;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
@@ -414,11 +411,7 @@
 
     private boolean mIsClickableAsLiveTile = true;
 
-    @Nullable private BorderAnimator mBorderAnimator;
-
-    private final boolean mCursorHoverStatesEnabled;
-
-    private final boolean mKeyboardFocusHighlightEnabled;
+    @Nullable private final BorderAnimator mBorderAnimator;
 
     public TaskView(Context context) {
         this(context, null);
@@ -441,29 +434,26 @@
         mCurrentFullscreenParams = new FullscreenDrawParams(context);
         mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
 
-        mKeyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
+        boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get()
                 || DesktopTaskView.DESKTOP_MODE_SUPPORTED;
-        mCursorHoverStatesEnabled = FeatureFlags.ENABLE_CURSOR_HOVER_STATES.get();
-        if (mCursorHoverStatesEnabled) {
-            setOnHoverListener(this::onHover);
-        }
 
-        setWillNotDraw(!mKeyboardFocusHighlightEnabled && !mCursorHoverStatesEnabled);
+        setWillNotDraw(!keyboardFocusHighlightEnabled);
 
-        if (mKeyboardFocusHighlightEnabled || mCursorHoverStatesEnabled) {
-            TypedArray ta = context.obtainStyledAttributes(
-                    attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
-            mBorderAnimator = new BorderAnimator(
-                    /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
-                    /* borderColor= */ ta.getColor(
-                    R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR),
-                    /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
-                    /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
-                    R.dimen.keyboard_quick_switch_border_width),
-                    /* boundsBuilder= */ this::updateBorderBounds,
-                    /* targetView= */ this));
-            ta.recycle();
-        }
+        TypedArray ta = context.obtainStyledAttributes(
+                attrs, R.styleable.TaskView, defStyleAttr, defStyleRes);
+
+        mBorderAnimator = !keyboardFocusHighlightEnabled
+                ? null
+                : new BorderAnimator(
+                        /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius,
+                        /* borderColor= */ ta.getColor(
+                                R.styleable.TaskView_borderColor, DEFAULT_BORDER_COLOR),
+                        /* borderAnimationParams= */ new BorderAnimator.SimpleParams(
+                                /* borderWidthPx= */ context.getResources().getDimensionPixelSize(
+                                        R.dimen.keyboard_quick_switch_border_width),
+                                /* boundsBuilder= */ this::updateBorderBounds,
+                                /* targetView= */ this));
+        ta.recycle();
     }
 
     protected void updateBorderBounds(Rect bounds) {
@@ -506,12 +496,6 @@
         return stubInfo;
     }
 
-    @Nullable
-    @VisibleForTesting
-    public BorderAnimator getBorderAnimator() {
-        return mBorderAnimator;
-    }
-
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
@@ -523,23 +507,12 @@
     @Override
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-        if (mKeyboardFocusHighlightEnabled) {
+        if (mBorderAnimator != null) {
             mBorderAnimator.buildAnimator(gainFocus).start();
         }
     }
 
     @Override
-    public boolean onInterceptHoverEvent(MotionEvent event) {
-        if (mCursorHoverStatesEnabled) {
-            // avoid triggering hover event on child elements which would cause HOVER_EXIT for this
-            // task view
-            return true;
-        } else {
-            return super.onInterceptHoverEvent(event);
-        }
-    }
-
-    @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
         if (mBorderAnimator != null) {
@@ -777,57 +750,6 @@
                 .log(LAUNCHER_TASK_LAUNCH_TAP);
     }
 
-    private boolean onHover(View v, MotionEvent event) {
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_HOVER_MOVE:
-                if (mKeyboardFocusHighlightEnabled && !isFocused()) {
-                    // existing focus is on another task selected by keyboard,
-                    // cursor then moves inside this task thumbnail and steals the focus
-                    requestFocusAndExitTouchMode(v);
-                }
-                return true;
-            case MotionEvent.ACTION_HOVER_ENTER:
-                if (mKeyboardFocusHighlightEnabled) {
-                    if (isFocused()) {
-                        // the task is already focused with border, no action is needed
-                        return true;
-                    } else {
-                        requestFocusAndExitTouchMode(v);
-                    }
-                } else {
-                    // mKeyboardFocusHighlightEnabled is turned off so it only shows hover
-                    // state animation but not steals the focus
-                    mBorderAnimator.buildAnimator(true).start();
-                }
-                return true;
-            case MotionEvent.ACTION_HOVER_EXIT:
-                if (mKeyboardFocusHighlightEnabled) {
-                    // clearFocus() does not work here because parent element is not focusable
-                    // so it changes to touch mode to clear focus
-                    v.getViewRootImpl().touchModeChanged(true);
-                } else {
-                    // just show the disappearing animation but not change the focus when
-                    // mKeyboardFocusHighlightEnabled is off
-                    mBorderAnimator.buildAnimator(false).start();
-                }
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    private void requestFocusAndExitTouchMode(View v) {
-        if (isInTouchMode()) {
-            // Tasks are not focusable in touch mode by default. As hover state would steal focus
-            // when both mKeyboardFocusHighlightEnabled and mCursorHoverStatesEnabled are on,
-            // touch mode needs to be set to false when hovering so it can steal focus to current
-            // task and show border animation as hover state
-            v.getViewRootImpl().touchModeChanged(false);
-        }
-
-        requestFocus();
-    }
-
     /**
      * @return {@code true} if user is already in split select mode and this tap was to choose the
      *         second app. {@code false} otherwise
@@ -1634,8 +1556,8 @@
             if (taskContainer == null) {
                 continue;
             }
-            for (SystemShortcut s : TraceHelper.allowIpcs(
-                    "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) {
+            for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
+                    taskContainer)) {
                 info.addAction(s.createAccessibilityAction(context));
             }
         }
@@ -1672,7 +1594,7 @@
             if (taskContainer == null) {
                 continue;
             }
-            for (SystemShortcut s : getEnabledShortcuts(this,
+            for (SystemShortcut s : TaskOverlayFactory.getEnabledShortcuts(this,
                     taskContainer)) {
                 if (s.hasHandlerForAction(action)) {
                     s.onClick(this);
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 5127190..df5303f 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -20,6 +20,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.RaceConditionReproducer;
 import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
 
 import org.junit.Before;
@@ -44,6 +45,18 @@
         startTestActivity(2);
     }
 
+    private void runTest(String... eventSequence) {
+        final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(eventSequence);
+
+        // Destroy Launcher activity.
+        closeLauncherActivity();
+
+        // The test action.
+        eventProcessor.startIteration();
+        mLauncher.goHome();
+        eventProcessor.finishIteration();
+    }
+
     @Ignore
     @Test
     @NavigationModeSwitch
diff --git a/res/drawable/ic_wallpaper.xml b/res/drawable/ic_wallpaper.xml
new file mode 100644
index 0000000..9543f88
--- /dev/null
+++ b/res/drawable/ic_wallpaper.xml
@@ -0,0 +1,27 @@
+<!--
+   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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="@dimen/options_menu_icon_size"
+        android:height="@dimen/options_menu_icon_size"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?android:attr/textColorPrimary">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M9,12.71l2.14,2.58l3-3.87L18,16.57H6L9,12.71z M5,5h6V3H5C3.9,3,3,3.9,3,5v6h2V5z M19,19h-6v2h6c1.1,0,2-0.9,2-2v-6h-2V19z
+            M5,19v-6H3v6c0,1.1,0.9,2,2,2h6v-2H5z M19,5v6h2V5c0-1.1-0.9-2-2-2h-6v2H19z M16,9c0.55,0,1-0.45,1-1s-0.45-1-1-1
+            c-0.55,0-1,0.45-1,1S15.45,9,16,9z"/>
+</vector>
diff --git a/res/values/config.xml b/res/values/config.xml
index 83f840d..5a6698b 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -102,6 +102,8 @@
 
     <!-- Default packages -->
     <string name="wallpaper_picker_package" translatable="false"></string>
+    <string name="custom_activity_picker" translatable="false">
+        com.android.customization.picker.CustomizationPickerActivity</string>
     <string name="local_colors_extraction_class" translatable="false"></string>
     <string name="search_session_manager_class" translatable="false"></string>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1b46b4d..c2eb373 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -250,6 +250,8 @@
 
     <!-- Strings for the customization mode -->
     <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
+    <string name="wallpaper_button_text">Wallpapers</string>
+    <!-- Text for wallpaper change button [CHAR LIMIT=30]-->
     <string name="styles_wallpaper_button_text">Wallpaper &amp; style</string>
     <!-- Text for edit home screen button [CHAR LIMIT=30]-->
     <string name="edit_home_screen">Edit Home Screen</string>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3ba8a1d..1da0ef4 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -133,6 +133,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.StringRes;
+import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DropTarget.DragObject;
@@ -205,6 +206,7 @@
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.launcher3.util.ScreenOnTracker;
@@ -246,6 +248,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -442,7 +445,8 @@
             Trace.beginAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
                     DISPLAY_ALL_APPS_TRACE_COOKIE);
         }
-        TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_CREATE_EVT,
+                TraceHelper.FLAG_UI_EVENT);
         if (DEBUG_STRICT_MODE) {
             StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                     .detectDiskReads()
@@ -576,7 +580,7 @@
                 LauncherOverlayPlugin.class, false /* allowedMultiple */);
 
         mRotationHelper.initialize();
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
 
         mUserChangedCallbackCloseable = UserCache.INSTANCE.get(this).addUserChangeListener(
                 () -> getStateManager().goToState(NORMAL));
@@ -1074,14 +1078,15 @@
 
     @Override
     protected void onStart() {
-        TraceHelper.INSTANCE.beginSection(ON_START_EVT);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_START_EVT,
+                TraceHelper.FLAG_UI_EVENT);
         super.onStart();
         if (!mDeferOverlayCallbacks) {
             mOverlayManager.onActivityStarted(this);
         }
 
         mAppWidgetHolder.setActivityStarted(true);
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     @Override
@@ -1252,7 +1257,8 @@
 
     @Override
     protected void onResume() {
-        TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_RESUME_EVT,
+                TraceHelper.FLAG_UI_EVENT);
         super.onResume();
 
         if (mDeferOverlayCallbacks) {
@@ -1262,7 +1268,7 @@
         }
 
         DragView.removeAllViews(this);
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     @Override
@@ -1650,7 +1656,7 @@
         if (Utilities.isRunningInTestHarness()) {
             Log.d(TestProtocol.PERMANENT_DIAG_TAG, "Launcher.onNewIntent: " + intent);
         }
-        TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(ON_NEW_INTENT_EVT);
         super.onNewIntent(intent);
 
         boolean alreadyOnHome = hasWindowFocus() && ((intent.getFlags() &
@@ -1697,7 +1703,7 @@
             showAllAppsWorkTabFromIntent(alreadyOnHome);
         }
 
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     protected void toggleAllAppsFromIntent(boolean alreadyOnHome) {
@@ -2296,7 +2302,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void startBinding() {
-        TraceHelper.INSTANCE.beginSection("startBinding");
+        Object traceToken = TraceHelper.INSTANCE.beginSection("startBinding");
         // Floating panels (except the full widget sheet) are associated with individual icons. If
         // we are starting a fresh bind, close all such panels as all the icons are about
         // to go away.
@@ -2314,7 +2320,7 @@
         if (mHotseat != null) {
             mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout());
         }
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     @Override
@@ -2567,7 +2573,7 @@
             return view;
         }
 
-        TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
+        Object traceToken = TraceHelper.INSTANCE.beginSection("BIND_WIDGET_id=" + item.appWidgetId);
 
         try {
             final LauncherAppWidgetProviderInfo appWidgetInfo;
@@ -2697,7 +2703,7 @@
             }
             prepareAppWidget(view, item);
         } finally {
-            TraceHelper.INSTANCE.endSection();
+            TraceHelper.INSTANCE.endSection(traceToken);
         }
 
         return view;
@@ -2790,7 +2796,7 @@
      * Implementation of the method from LauncherModel.Callbacks.
      */
     public void finishBindingItems(IntSet pagesBoundFirst) {
-        TraceHelper.INSTANCE.beginSection("finishBindingItems");
+        Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems");
         mWorkspace.restoreInstanceStateForRemainingPages();
 
         setWorkspaceLoading(false);
@@ -2815,7 +2821,7 @@
                 mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
         getViewCache().setCacheSize(R.layout.folder_page, 2);
 
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
 
         mWorkspace.removeExtraEmptyScreen(true);
     }
@@ -2967,8 +2973,12 @@
      */
     @Override
     @TargetApi(Build.VERSION_CODES.S)
-    public void bindAllApplications(AppInfo[] apps, int flags) {
-        mAppsView.getAppsStore().setApps(apps, flags);
+    @UiThread
+    public void bindAllApplications(AppInfo[] apps, int flags,
+            Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
+        Preconditions.assertUIThread();
+        AllAppsStore appsStore = mAppsView.getAppsStore();
+        appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
         if (Utilities.ATLEAST_S) {
             Trace.endAsyncSection(DISPLAY_ALL_APPS_TRACE_METHOD_NAME,
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index c20494d..4e066b0 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
 import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
+import static com.android.launcher3.testing.shared.TestProtocol.sDebugTracing;
 import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
@@ -295,7 +296,7 @@
     }
 
     public void onBroadcastIntent(@NonNull final Intent intent) {
-        if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent);
+        if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=" + intent);
         final String action = intent.getAction();
         if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
             // If we have changed locale we need to clear out the labels in all apps/workspace.
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index 21dfbe1..d4140d8 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -444,6 +444,9 @@
             // Animate to A-Z with 0 time to reset the animation with proper state management.
             animateToSearchState(false, 0);
         }
+        if (isSearching()) {
+            mWorkManager.reset();
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsStore.java b/src/com/android/launcher3/allapps/AllAppsStore.java
index a977b3a..06af970 100644
--- a/src/com/android/launcher3/allapps/AllAppsStore.java
+++ b/src/com/android/launcher3/allapps/AllAppsStore.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
 import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
 
+import android.os.UserHandle;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -35,7 +36,9 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
@@ -57,8 +60,8 @@
 
     private final List<OnUpdateListener> mUpdateListeners = new CopyOnWriteArrayList<>();
     private final ArrayList<ViewGroup> mIconContainers = new ArrayList<>();
+    private Map<PackageUserKey, Integer> mPackageUserKeytoUidMap = Collections.emptyMap();
     private int mModelFlags;
-
     private int mDeferUpdatesFlags = 0;
     private boolean mUpdatePending = false;
 
@@ -67,12 +70,21 @@
     }
 
     /**
-     * Sets the current set of apps.
+     * Sets the current set of apps and sets mapping for {@link PackageUserKey} to Uid for
+     * the current set of apps.
      */
-    public void setApps(AppInfo[] apps, int flags) {
+    public void setApps(AppInfo[] apps, int flags, Map<PackageUserKey, Integer> map) {
         mApps = apps;
         mModelFlags = flags;
         notifyUpdate();
+        mPackageUserKeytoUidMap = map;
+    }
+
+    /**
+     * Look up for Uid using package name and user handle for the current set of apps.
+     */
+    public int lookUpForUid(String packageName, UserHandle user) {
+        return mPackageUserKeytoUidMap.getOrDefault(new PackageUserKey(packageName, user), -1);
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 663fdb9..8c2fb19 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -166,7 +166,7 @@
         return super.onApplyWindowInsets(insets);
     }
 
-    private void updateTranslationY() {
+    void updateTranslationY() {
         setTranslationY(-mImeInsets.bottom);
     }
 
@@ -180,6 +180,10 @@
         rect.set(insets.left, insets.top, insets.right, insets.bottom);
     }
 
+    public Rect getImeInsets() {
+        return mImeInsets;
+    }
+
     @Override
     public void onTranslationStart() {
         setFlag(FLAG_TRANSLATION_ONGOING);
diff --git a/src/com/android/launcher3/allapps/WorkProfileManager.java b/src/com/android/launcher3/allapps/WorkProfileManager.java
index 30af502..44c233f 100644
--- a/src/com/android/launcher3/allapps/WorkProfileManager.java
+++ b/src/com/android/launcher3/allapps/WorkProfileManager.java
@@ -136,6 +136,11 @@
     public void reset() {
         boolean isEnabled = !mAllApps.getAppsStore().hasModelFlag(FLAG_QUIET_MODE_ENABLED);
         updateCurrentState(isEnabled ? STATE_ENABLED : STATE_DISABLED);
+        if (mWorkModeSwitch != null) {
+            // reset the position of the button and clear IME insets.
+            mWorkModeSwitch.getImeInsets().setEmpty();
+            mWorkModeSwitch.updateTranslationY();
+        }
     }
 
     private void updateCurrentState(@WorkProfileState int currentState) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 80b6452..b8d2df0 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -314,7 +314,7 @@
             "Enable a grid-only overview without a focused task.");
 
     public static final BooleanFlag ENABLE_CURSOR_HOVER_STATES = getDebugFlag(243191650,
-            "ENABLE_CURSOR_HOVER_STATES", ENABLED,
+            "ENABLE_CURSOR_HOVER_STATES", TEAMFOOD,
             "Enables cursor hover states for certain elements.");
 
     // TODO(Block 24): Clean up flags
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 7103670..7e065a9 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -983,6 +983,14 @@
         }
 
         /**
+         * Sets list of uid for each of {@link com.android.app.search.ResultType} that indicates
+         * package name for the impression event.
+         */
+        default StatsImpressionLogger withUids(IntArray uid) {
+            return this;
+        }
+
+        /**
          * Builds the final message and logs it as {@link EventEnum}.
          */
         default void log(EventEnum event) {
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 85def73..6935353 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -23,6 +23,8 @@
 import android.os.Process;
 import android.util.Log;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.launcher3.InvariantDeviceProfile;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel.CallbackTask;
@@ -34,20 +36,24 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.LooperExecutor;
 import com.android.launcher3.util.LooperIdleLock;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 /**
  * Binds the results of {@link com.android.launcher3.model.LoaderTask} to the Callbacks objects.
@@ -143,11 +149,18 @@
     /**
      * Binds the all apps results from LoaderTask to the callbacks UX.
      */
+    @WorkerThread
     public void bindAllApps() {
+        Preconditions.assertWorkerThread();
         // shallow copy
         AppInfo[] apps = mBgAllAppsList.copyData();
         int flags = mBgAllAppsList.getFlags();
-        executeCallbacksTask(c -> c.bindAllApplications(apps, flags), mUiExecutor);
+        Map<PackageUserKey, Integer> packageUserKeytoUidMap = Arrays.stream(apps).collect(
+                Collectors.toMap(
+                        appInfo -> new PackageUserKey(appInfo.componentName.getPackageName(),
+                                appInfo.user), appInfo -> appInfo.uid, (a, b) -> a));
+        executeCallbacksTask(c -> c.bindAllApplications(apps, flags, packageUserKeytoUidMap),
+                mUiExecutor);
     }
 
     /**
@@ -268,33 +281,18 @@
             sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
             sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
 
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "Before posting startBinding");
-            }
             // Tell the workspace that we're about to start binding items
             executeCallbacksTask(c -> {
                 c.clearPendingBinds();
                 c.startBinding();
             }, mUiExecutor);
 
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "1");
-            }
             // Bind workspace screens
             executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
 
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "2");
-            }
             // Load items on the current page.
             bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "3");
-            }
             bindAppWidgets(currentAppWidgets, mUiExecutor);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "4");
-            }
             if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
                 mExtraItems.forEach(item ->
                         executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
@@ -303,18 +301,8 @@
             RunnableList pendingTasks = new RunnableList();
             Executor pendingExecutor = pendingTasks::add;
             bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
-
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "5");
-            }
             bindAppWidgets(otherAppWidgets, pendingExecutor);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "6");
-            }
             executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "After posting finishBindingItems");
-            }
             pendingExecutor.execute(
                     () -> {
                         MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index 44d32d9..1ba015a 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
@@ -35,11 +36,15 @@
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Predicate;
@@ -153,7 +158,9 @@
         scheduleCallbackTask(c -> c.bindWorkspaceComponentsRemoved(matcher));
     }
 
+    @WorkerThread
     public void bindApplicationsIfNeeded() {
+        Preconditions.assertWorkerThread();
         boolean changeFlag = mAllAppsList.getAndResetChangeFlag();
         if (TestProtocol.sDebugTracing) {
             Log.d(WORK_TAB_MISSING, "bindApplicationsIfNeeded changeFlag? " +
@@ -162,7 +169,11 @@
         if (changeFlag) {
             AppInfo[] apps = mAllAppsList.copyData();
             int flags = mAllAppsList.getFlags();
-            scheduleCallbackTask(c -> c.bindAllApplications(apps, flags));
+            Map<PackageUserKey, Integer> packageUserKeytoUidMap = Arrays.stream(apps).collect(
+                    Collectors.toMap(
+                            appInfo -> new PackageUserKey(appInfo.componentName.getPackageName(),
+                                    appInfo.user), appInfo -> appInfo.uid, (a, b) -> a));
+            scheduleCallbackTask(c -> c.bindAllApplications(apps, flags, packageUserKeytoUidMap));
         }
     }
 }
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 5b0da5b..1f56707 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -51,6 +51,7 @@
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.widget.model.WidgetsListBaseEntry;
 
@@ -495,7 +496,9 @@
          */
         default void bindExtraContainerItems(FixedContainerItems item) { }
 
-        default void bindAllApplications(AppInfo[] apps, int flags) { }
+        default void bindAllApplications(AppInfo[] apps, int flags,
+                Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
+        }
 
         /**
          * Binds the cache of string resources
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 73a06b6..91fd65d 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -24,6 +24,8 @@
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
+import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
 import static com.android.launcher3.util.PackageManagerHelper.isSystemApp;
@@ -201,7 +203,7 @@
             }
         }
 
-        TraceHelper.INSTANCE.beginSection(TAG);
+        Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
         LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
         try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
             List<ShortcutInfo> allShortcuts = new ArrayList<>();
@@ -325,7 +327,7 @@
             memoryLogger.printLogs();
             throw e;
         }
-        TraceHelper.INSTANCE.endSection();
+        TraceHelper.INSTANCE.endSection(traceToken);
     }
 
     public synchronized void stopLocked() {
@@ -947,6 +949,7 @@
     }
 
     private List<LauncherActivityInfo> loadAllApps() {
+        testLogD(WORK_TAB_MISSING, "loadingAllApps");
         final List<UserHandle> profiles = mUserCache.getUserProfiles();
         List<LauncherActivityInfo> allActivityList = new ArrayList<>();
         // Clear the list of apps
diff --git a/src/com/android/launcher3/model/data/AppInfo.java b/src/com/android/launcher3/model/data/AppInfo.java
index 34972e7..7e6cbef 100644
--- a/src/com/android/launcher3/model/data/AppInfo.java
+++ b/src/com/android/launcher3/model/data/AppInfo.java
@@ -61,6 +61,13 @@
     // Section name used for indexing.
     public String sectionName = "";
 
+    /**
+     * The uid of the application.
+     * The kernel user-ID that has been assigned to this application. Currently this is not a unique
+     * ID (multiple applications can have the same uid).
+     */
+    public int uid = -1;
+
     public AppInfo() {
         itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
     }
@@ -87,6 +94,7 @@
         if (quietModeEnabled) {
             runtimeStatusFlags |= FLAG_DISABLED_QUIET_USER;
         }
+        uid = info.getApplicationInfo().uid;
         updateRuntimeFlagsForActivityTarget(this, info);
     }
 
@@ -95,6 +103,7 @@
         componentName = info.componentName;
         title = Utilities.trim(info.title);
         intent = new Intent(info.intent);
+        uid = info.uid;
     }
 
     @VisibleForTesting
diff --git a/src/com/android/launcher3/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 5aab41a..5ebcf42 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -16,6 +16,9 @@
 
 package com.android.launcher3.pm;
 
+import static com.android.launcher3.testing.shared.TestProtocol.WORK_TAB_MISSING;
+import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
+
 import android.content.Context;
 import android.content.Intent;
 import android.os.UserHandle;
@@ -56,6 +59,7 @@
     }
 
     private void onUsersChanged(Intent intent) {
+        testLogD(WORK_TAB_MISSING, "onUsersChanged intent: " + intent);
         enableAndResetCache();
         mUserChangeListeners.forEach(Runnable::run);
     }
@@ -84,6 +88,7 @@
             List<UserHandle> users = mUserManager.getUserProfiles();
             if (users != null) {
                 for (UserHandle user : users) {
+                    testLogD(WORK_TAB_MISSING, "caching user: " + user);
                     long serial = mUserManager.getSerialNumberForUser(user);
                     mUsers.put(serial, user);
                     mUserToSerialMap.put(user, serial);
@@ -134,13 +139,24 @@
      * @see UserManager#getUserProfiles()
      */
     public List<UserHandle> getUserProfiles() {
+        StringBuilder usersToReturn = new StringBuilder();
         synchronized (this) {
             if (mUsers != null) {
+                for (UserHandle u : mUserToSerialMap.keySet()) {
+                    usersToReturn.append(u).append(" && ");
+                }
+                testLogD(WORK_TAB_MISSING, "users from cache: " + usersToReturn);
                 return new ArrayList<>(mUserToSerialMap.keySet());
+            } else {
+                testLogD(WORK_TAB_MISSING, "users from cache null");
             }
         }
 
         List<UserHandle> users = mUserManager.getUserProfiles();
+        for (UserHandle u : users) {
+            usersToReturn.append(u).append(" && ");
+        }
+        testLogD(WORK_TAB_MISSING, "users from userManager: " + usersToReturn);
         return users == null ? Collections.emptyList() : users;
     }
 }
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index fcc62a7..458f137 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -26,6 +26,8 @@
 import android.view.ViewAnimationUtils;
 import android.view.inputmethod.InputMethodManager;
 
+import androidx.annotation.UiThread;
+
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.BubbleTextView;
@@ -38,6 +40,7 @@
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.ActivityAllAppsContainerView;
+import com.android.launcher3.allapps.AllAppsStore;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DraggableView;
@@ -54,10 +57,13 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.OnboardingPrefs;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer;
 
 import java.util.HashMap;
+import java.util.Map;
 
 /**
  * Launcher activity for secondary displays
@@ -291,9 +297,13 @@
         mPopupDataProvider.setDeepShortcutMap(deepShortcutMap);
     }
 
+    @UiThread
     @Override
-    public void bindAllApplications(AppInfo[] apps, int flags) {
-        mAppsView.getAppsStore().setApps(apps, flags);
+    public void bindAllApplications(AppInfo[] apps, int flags,
+            Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
+        Preconditions.assertUIThread();
+        AllAppsStore appsStore = mAppsView.getAppsStore();
+        appsStore.setApps(apps, flags, packageUserKeytoUidMap);
         PopupContainerWithArrow.dismissInvalidPopup(this);
     }
 
diff --git a/src/com/android/launcher3/settings/SettingsActivity.java b/src/com/android/launcher3/settings/SettingsActivity.java
index d3a237c..623b557 100644
--- a/src/com/android/launcher3/settings/SettingsActivity.java
+++ b/src/com/android/launcher3/settings/SettingsActivity.java
@@ -249,6 +249,8 @@
                         bottomPadding + insets.getSystemWindowInsetBottom());
                 return insets.consumeSystemWindowInsets();
             });
+            // Overriding Text Direction in the Androidx preference library to support RTL
+            view.setTextDirection(View.TEXT_DIRECTION_LOCALE);
         }
 
         @Override
diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java
index bdb5e77..6f706d2 100644
--- a/src/com/android/launcher3/util/InstantAppResolver.java
+++ b/src/com/android/launcher3/util/InstantAppResolver.java
@@ -42,7 +42,14 @@
         return false;
     }
 
-    public boolean isInstantApp(String packageName, int userId) {
+    public boolean isInstantApp(Context context, String packageName) {
+        PackageManager packageManager = context.getPackageManager();
+        try {
+            return isInstantApp(packageManager.getPackageInfo(packageName, 0).applicationInfo);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e("InstantAppResolver", "Failed to determine whether package is instant app "
+                    + packageName, e);
+        }
         return false;
     }
 }
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index 8a27381..b6af314 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -18,7 +18,6 @@
 
 import android.content.ComponentName;
 import android.os.UserHandle;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 
@@ -26,7 +25,6 @@
 import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.testing.shared.TestProtocol;
 
 import java.util.Collection;
 import java.util.HashSet;
@@ -44,14 +42,7 @@
     private static final ComponentName EMPTY_COMPONENT = new ComponentName("", "");
 
     public static Predicate<ItemInfo> ofUser(UserHandle user) {
-        return info -> {
-            if (TestProtocol.sDebugTracing) {
-                Log.d(TestProtocol.WORK_TAB_MISSING, "userHandle: " + user
-                        + ", itemUserHandle: " + info.user
-                        + " package: " + info.getTargetPackage());
-            }
-            return info != null && info.user.equals(user);
-        };
+        return info -> info != null && info.user.equals(user);
     }
 
     public static Predicate<ItemInfo> ofComponents(
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 91203a7..1d6bc25 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -164,6 +164,13 @@
         }
     }
 
+    public static Intent getStyleWallpapersIntent(Context context) {
+        return new Intent(Intent.ACTION_SET_WALLPAPER).setComponent(
+                new ComponentName(context.getString(R.string.wallpaper_picker_package),
+                    context.getString(R.string.custom_activity_picker)
+                ));
+    }
+
     /**
      * Starts the details activity for {@code info}
      */
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index d5056ee..c23df77 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -21,8 +21,6 @@
 
 import java.util.function.Supplier;
 
-import kotlin.random.Random;
-
 /**
  * A wrapper around {@link Trace} to allow better testing.
  *
@@ -38,53 +36,54 @@
     // Temporarily ignore blocking binder calls for this trace.
     public static final int FLAG_IGNORE_BINDERS = 1 << 1;
 
+    public static final int FLAG_CHECK_FOR_RACE_CONDITIONS = 1 << 2;
+
+    public static final int FLAG_UI_EVENT =
+            FLAG_ALLOW_BINDER_TRACKING | FLAG_CHECK_FOR_RACE_CONDITIONS;
+
     /**
      * Static instance of Trace helper, overridden in tests.
      */
     public static TraceHelper INSTANCE = new TraceHelper();
 
     /**
-     * @see Trace#beginSection(String)
+     * @return a token to pass into {@link #endSection(Object)}.
      */
-    public void beginSection(String sectionName) {
+    public Object beginSection(String sectionName) {
+        return beginSection(sectionName, 0);
+    }
+
+    public Object beginSection(String sectionName, int flags) {
         Trace.beginSection(sectionName);
+        return null;
     }
 
     /**
-     * @see Trace#endSection()
+     * @param token the token returned from {@link #beginSection(String, int)}
      */
-    public void endSection() {
+    public void endSection(Object token) {
         Trace.endSection();
     }
 
     /**
-     * @see Trace#beginAsyncSection(String, int)
-     * @return a SafeCloseable that can be used to end the session
+     * Similar to {@link #beginSection} but doesn't add a trace section.
      */
-    public SafeCloseable beginAsyncSection(String sectionName) {
-        int cookie = Random.Default.nextInt();
-        Trace.beginAsyncSection(sectionName, cookie);
-        return () -> Trace.endAsyncSection(sectionName, cookie);
+    public Object beginFlagsOverride(int flags) {
+        return null;
     }
 
-    /**
-     * Returns a SafeCloseable to temporarily ignore blocking binder calls.
-     */
-    public SafeCloseable allowIpcs(String rpcName) {
-        int cookie = Random.Default.nextInt();
-        Trace.beginAsyncSection(rpcName, cookie);
-        return () -> Trace.endAsyncSection(rpcName, cookie);
-    }
+    public void endFlagsOverride(Object token) { }
 
     /**
      * Temporarily ignore blocking binder calls for the duration of this {@link Supplier}.
-     *
-     * Note, new features should be designed to not rely on mainThread RPCs.
      */
     @MainThread
     public static <T> T allowIpcs(String rpcName, Supplier<T> supplier) {
-        try (SafeCloseable c = INSTANCE.allowIpcs(rpcName)) {
+        Object traceToken = INSTANCE.beginSection(rpcName, FLAG_IGNORE_BINDERS);
+        try {
             return supplier.get();
+        } finally {
+            INSTANCE.endSection(traceToken);
         }
     }
 }
diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java
index 55febc7..aebf752 100644
--- a/src/com/android/launcher3/views/OptionsPopupView.java
+++ b/src/com/android/launcher3/views/OptionsPopupView.java
@@ -55,6 +55,7 @@
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.testing.TestLogging;
 import com.android.launcher3.testing.shared.TestProtocol;
+import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.widget.picker.WidgetsFullSheet;
 
 import java.util.ArrayList;
@@ -189,9 +190,14 @@
      */
     public static ArrayList<OptionItem> getOptions(Launcher launcher) {
         ArrayList<OptionItem> options = new ArrayList<>();
+        boolean styleWallpaperExists = styleWallpapersExists(launcher);
+        int resString = styleWallpaperExists
+                ? R.string.styles_wallpaper_button_text : R.string.wallpaper_button_text;
+        int resDrawable = styleWallpaperExists
+                ? R.drawable.ic_palette : R.drawable.ic_wallpaper;
         options.add(new OptionItem(launcher,
-                R.string.styles_wallpaper_button_text,
-                R.drawable.ic_palette,
+                resString,
+                resDrawable,
                 IGNORE,
                 OptionsPopupView::startWallpaperPicker));
         if (!WidgetsModel.GO_DISABLE_WIDGETS) {
@@ -268,8 +274,12 @@
                 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
                 .putExtra(EXTRA_WALLPAPER_OFFSET,
                         launcher.getWorkspace().getWallpaperOffsetForCenterPage())
-                .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher")
-                .putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
+                .putExtra(EXTRA_WALLPAPER_LAUNCH_SOURCE, "app_launched_launcher");
+        if (!styleWallpapersExists(launcher)) {
+            intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "wallpaper_only");
+        } else {
+            intent.putExtra(EXTRA_WALLPAPER_FLAVOR, "focus_wallpaper");
+        }
         String pickerPackage = launcher.getString(R.string.wallpaper_picker_package);
         if (!TextUtils.isEmpty(pickerPackage)) {
             intent.setPackage(pickerPackage);
@@ -312,4 +322,9 @@
             this.clickListener = clickListener;
         }
     }
+
+    private static boolean styleWallpapersExists(Context context) {
+        return context.getPackageManager().resolveActivity(
+                PackageManagerHelper.getStyleWallpapersIntent(context), 0) != null;
+    }
 }
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 7abdf8a..7f2d4a0 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -140,10 +140,6 @@
     public static final String REQUEST_GET_GRID_TASK_SIZE_RECT_FOR_TABLET =
             "get-grid-task-size-rect-for-tablet";
     public static final String REQUEST_GET_OVERVIEW_PAGE_SPACING = "get-overview-page-spacing";
-
-    public static final String REQUEST_GET_OVERVIEW_TASK_BORDER_WIDTH =
-            "get-overview-task-border-width";
-
     public static final String REQUEST_ENABLE_ROTATION = "enable_rotation";
     public static final String REQUEST_ENABLE_SUGGESTION = "enable-suggestion";
     public static final String REQUEST_MODEL_QUEUE_CLEARED = "model-queue-cleared";
@@ -159,7 +155,6 @@
     public static final String PERMANENT_DIAG_TAG = "TaplTarget";
     public static final String WORK_TAB_MISSING = "b/243688989";
     public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
-    public static final String WORKSPACE_LOADS_FOREVER = "b/267200150";
 
     public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
     public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
index c4aa42b..243edc3 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
@@ -28,7 +28,6 @@
 import com.android.launcher3.celllayout.testcases.MultipleCellLayoutsSimpleReorder;
 import com.android.launcher3.tapl.Widget;
 import com.android.launcher3.tapl.WidgetResizeFrame;
-import com.android.launcher3.testing.shared.TestProtocol;
 import com.android.launcher3.ui.AbstractLauncherUiTest;
 import com.android.launcher3.ui.TaplTestsLauncher3;
 import com.android.launcher3.util.rule.ShellCommandRule;
@@ -115,11 +114,8 @@
         // resetLoaderState triggers the launcher to start loading the workspace which allows
         // waitForLauncherCondition to wait for that condition, otherwise the condition would
         // always be true and it wouldn't wait for the changes to be applied.
-        Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "before resetLoaderState");
         resetLoaderState();
-        Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "after resetLoaderState");
         waitForLauncherCondition("Workspace didn't finish loading", l -> !l.isWorkspaceLoading());
-        Log.d(TestProtocol.WORKSPACE_LOADS_FOREVER, "after waitForLauncherCondition");
         Widget widget = mLauncher.getWorkspace().getWidgetAtCell(mainWidgetCellPos.getCellX(),
                 mainWidgetCellPos.getCellY());
         assertNotNull(widget);
diff --git a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index b9da16a..3dfd6b4 100644
--- a/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.LauncherLayoutBuilder;
 import com.android.launcher3.util.LauncherModelHelper;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.TestUtil;
 
@@ -48,6 +49,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -192,7 +194,8 @@
         }
 
         @Override
-        public void bindAllApplications(AppInfo[] apps, int flags) {
+        public void bindAllApplications(AppInfo[] apps, int flags,
+                Map<PackageUserKey, Integer> packageUserKeytoUidMap) {
             mAppInfos = apps;
         }
 
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducer.java b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
new file mode 100644
index 0000000..ed2ec7b
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducer.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static com.android.launcher3.util.Executors.createAndStartNewLooper;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Event processor for reliably reproducing multithreaded apps race conditions in tests.
+ *
+ * The app notifies us about “events” that happen in its threads. The race condition test runs the
+ * test action multiple times (aka iterations), trying to generate all possible permutations of
+ * these events. It keeps a set of all seen event sequences and steers the execution towards
+ * executing events in previously unseen order. It does it by postponing execution of threads that
+ * would lead to an already seen sequence.
+ *
+ * If an event A occurs before event B in the sequence, this is how execution order looks like:
+ * Events: ... A ... B ...
+ * Events and instructions, guaranteed order:
+ * (instructions executed prior to A) A ... B (instructions executed after B)
+ *
+ * Each iteration has 3 parts (phases).
+ * Phase 1. Picking a previously seen event subsequence that we believe can have previously unseen
+ * continuations. Reproducing this sequence by pausing threads that would lead to other sequences.
+ * Phase 2. Trying to generate previously unseen continuation of the sequence from Phase 1. We need
+ * one new event after that sequence. All threads leading to seen continuations will be postponed
+ * for some short period of time. The phase ends once the new event is registered, or after the
+ * period of time ends (in which case we declare that the sequence can’t have new continuations).
+ * Phase 3. Releasing all threads and letting the test iteration run till its end.
+ *
+ * The iterations end when all seen paths have been declared “uncontinuable”.
+ *
+ * When we register event XXX:enter, we hold all other events until we register XXX:exit.
+ */
+public class RaceConditionReproducer {
+    private static final String TAG = "RaceConditionReproducer";
+
+    private static final boolean ENTER = true;
+    private static final boolean EXIT = false;
+    private static final String ENTER_POSTFIX = "enter";
+    private static final String EXIT_POSTFIX = "exit";
+
+    private static final long SHORT_TIMEOUT_MS = 2000;
+    private static final long LONG_TIMEOUT_MS = 60000;
+    // Handler used to resume postponed events.
+    private static final Handler POSTPONED_EVENT_RESUME_HANDLER =
+            new Handler(createAndStartNewLooper("RaceConditionEventResumer"));
+
+    public static String enterExitEvt(String eventName, boolean isEnter) {
+        return eventName + ":" + (isEnter ? ENTER_POSTFIX : EXIT_POSTFIX);
+    }
+
+    public static String enterEvt(String eventName) {
+        return enterExitEvt(eventName, ENTER);
+    }
+
+    public static String exitEvt(String eventName) {
+        return enterExitEvt(eventName, EXIT);
+    }
+
+    /**
+     * Event in a particular sequence of events. A node in the prefix tree of all seen event
+     * sequences.
+     */
+    private class EventNode {
+        // Events that were seen just after this event.
+        private final Map<String, EventNode> mNextEvents = new HashMap<>();
+        // Whether we believe that further iterations will not be able to add more events to
+        // mNextEvents.
+        private boolean mStoppedAddingChildren = true;
+
+        private void debugDump(StringBuilder sb, int indent, String name) {
+            for (int i = 0; i < indent; ++i) sb.append('.');
+            sb.append(!mStoppedAddingChildren ? "+" : "-");
+            sb.append(" : ");
+            sb.append(name);
+            if (mLastRegisteredEvent == this) sb.append(" <");
+            sb.append('\n');
+
+            for (String key : mNextEvents.keySet()) {
+                mNextEvents.get(key).debugDump(sb, indent + 2, key);
+            }
+        }
+
+        /** Number of leaves in the subtree with this node as a root. */
+        private int numberOfLeafNodes() {
+            if (mNextEvents.isEmpty()) return 1;
+
+            int leaves = 0;
+            for (String event : mNextEvents.keySet()) {
+                leaves += mNextEvents.get(event).numberOfLeafNodes();
+            }
+            return leaves;
+        }
+
+        /**
+         * Whether we believe that further iterations will not be able add nodes to the subtree with
+         * this node as a root.
+         */
+        private boolean stoppedAddingChildrenToTree() {
+            if (!mStoppedAddingChildren) return false;
+
+            for (String event : mNextEvents.keySet()) {
+                if (!mNextEvents.get(event).stoppedAddingChildrenToTree()) return false;
+            }
+            return true;
+        }
+
+        /**
+         * In the subtree with this node as a root, tries finding a node where we may have a
+         * chance to add new children.
+         * If succeeds, returns true and fills 'path' with the sequence of events to that node;
+         * otherwise returns false.
+         */
+        private boolean populatePathToGrowthPoint(List<String> path) {
+            for (String event : mNextEvents.keySet()) {
+                if (mNextEvents.get(event).populatePathToGrowthPoint(path)) {
+                    path.add(0, event);
+                    return true;
+                }
+            }
+            if (!mStoppedAddingChildren) {
+                // Mark that we have finished adding children. It will remain true if no new
+                // children are added, or will be set to false upon adding a new child.
+                mStoppedAddingChildren = true;
+                return true;
+            }
+            return false;
+        }
+    }
+
+    // Starting point of all event sequences; the root of the prefix tree representation all
+    // sequences generated by test iterations. A test iteration can add nodes int it.
+    private EventNode mRoot = new EventNode();
+    // During a test iteration, the last event that was registered.
+    private EventNode mLastRegisteredEvent;
+    // Length of the current sequence of registered events for the current test iteration.
+    private int mRegisteredEventCount = 0;
+    // During the first part of a test iteration, we go to a specific node under mRoot by
+    // 'playing back' mSequenceToFollow. During this part, all events that don't belong to this
+    // sequence get postponed.
+    private List<String> mSequenceToFollow = new ArrayList<>();
+    // Collection of events that got postponed, with corresponding wait objects used to let them go.
+    private Map<String, Semaphore> mPostponedEvents = new HashMap<>();
+    // Callback to run by POSTPONED_EVENT_RESUME_HANDLER, used to let go of all currently
+    // postponed events.
+    private Runnable mResumeAllEventsCallback;
+    // String representation of the sequence of events registered so far for the current test
+    // iteration. After registering any event, we output it to the log. The last output before
+    // the test failure can be later played back to reliable reproduce the exact sequence of
+    // events that broke the test.
+    // Format: EV1|EV2|...\EVN
+    private StringBuilder mCurrentSequence;
+    // When not null, we are in a repro mode. We run only one test iteration, and are trying to
+    // reproduce the event sequence represented by this string. The format is same as for
+    // mCurrentSequence.
+    private final String mReproString;
+
+    /* Constructor for a normal test. */
+    public RaceConditionReproducer() {
+        mReproString = null;
+    }
+
+    /**
+     * Constructor for reliably reproducing a race condition failure. The developer should find in
+     * the log the latest "Repro sequence:" record and locally modify the test by passing that
+     * string to the constructor. Running the test will have only one iteration that will reliably
+     * "play back" that sequence.
+     */
+    public RaceConditionReproducer(String reproString) {
+        mReproString = reproString;
+    }
+
+    public RaceConditionReproducer(String... reproSequence) {
+        this(String.join("|", reproSequence));
+    }
+
+    public synchronized String getCurrentSequenceString() {
+        return mCurrentSequence.toString();
+    }
+
+    /**
+     * Starts a new test iteration. Events reported via RaceConditionTracker.onEvent before this
+     * call will be ignored.
+     */
+    public synchronized void startIteration() {
+        mLastRegisteredEvent = mRoot;
+        mRegisteredEventCount = 0;
+        mCurrentSequence = new StringBuilder();
+        Log.d(TAG, "Repro sequence: " + mCurrentSequence);
+        mSequenceToFollow = mReproString != null ?
+                parseReproString(mReproString) : generateSequenceToFollowLocked();
+        Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked());
+        checkIfCompletedSequenceToFollowLocked();
+
+        TraceHelperForTest.setRaceConditionReproducer(this);
+    }
+
+    /**
+     * Ends a new test iteration. Events reported via RaceConditionTracker.onEvent after this call
+     * will be ignored.
+     * Returns whether we need more iterations.
+     */
+    public synchronized boolean finishIteration() {
+        TraceHelperForTest.setRaceConditionReproducer(null);
+
+        runResumeAllEventsCallbackLocked();
+        assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty());
+        assertTrue("Last registered event is :enter", lastEventAsEnter() == null);
+
+        // No events came after mLastRegisteredEvent. It doesn't make sense to come to it again
+        // because we won't see new continuations.
+        mLastRegisteredEvent.mStoppedAddingChildren = true;
+        Log.e(TAG, "---- End of iteration; state:\n" + dumpStateLocked());
+        if (mReproString != null) {
+            assertTrue("Repro mode: failed to reproduce the sequence",
+                    mCurrentSequence.toString().startsWith(mReproString));
+        }
+        // If we are in a repro mode, we need only one iteration. Otherwise, continue if the tree
+        // has prospective growth points.
+        return mReproString == null && !mRoot.stoppedAddingChildrenToTree();
+    }
+
+    private static List<String> parseReproString(String reproString) {
+        return Arrays.asList(reproString.split("\\|"));
+    }
+
+    /**
+     * Called when the app issues an event.
+     */
+    public void onEvent(String event) {
+        final Semaphore waitObject = tryRegisterEvent(event);
+        if (waitObject != null) {
+            waitUntilCanRegister(event, waitObject);
+        }
+    }
+
+    /**
+     * Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit.
+     */
+    private boolean canRegisterEventNowLocked(String event) {
+        final String lastEventAsEnter = lastEventAsEnter();
+        final String thisEventAsExit = eventAsExit(event);
+
+        if (lastEventAsEnter != null) {
+            if (!lastEventAsEnter.equals(thisEventAsExit)) {
+                assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null);
+                // Last event was :enter, but this event is not :exit.
+                return false;
+            }
+        } else {
+            // Previous event was not :enter.
+            assertTrue(":exit after a non-enter event", thisEventAsExit == null);
+        }
+        return true;
+    }
+
+    /**
+     * Registers an event issued by the app and returns null or decides that the event must be
+     * postponed, and returns an object to wait on.
+     */
+    private synchronized Semaphore tryRegisterEvent(String event) {
+        Log.d(TAG, "Event issued by the app: " + event);
+
+        if (!canRegisterEventNowLocked(event)) {
+            return createWaitObjectForPostponedEventLocked(event);
+        }
+
+        if (mRegisteredEventCount < mSequenceToFollow.size()) {
+            // We are in the first part of the iteration. We only register events that follow the
+            // mSequenceToFollow and postponing all other events.
+            if (event.equals(mSequenceToFollow.get(mRegisteredEventCount))) {
+                // The event is the next one expected in the sequence. Register it.
+                registerEventLocked(event);
+
+                // If there are postponed events that could continue the sequence, register them.
+                while (mRegisteredEventCount < mSequenceToFollow.size() &&
+                        mPostponedEvents.containsKey(
+                                mSequenceToFollow.get(mRegisteredEventCount))) {
+                    registerPostponedEventLocked(mSequenceToFollow.get(mRegisteredEventCount));
+                }
+
+                // Perhaps we just completed the required sequence...
+                checkIfCompletedSequenceToFollowLocked();
+            } else {
+                // The event is not the next one in the sequence. Postpone it.
+                return createWaitObjectForPostponedEventLocked(event);
+            }
+        } else if (mRegisteredEventCount == mSequenceToFollow.size()) {
+            // The second phase of the iteration. We have just registered the whole
+            // mSequenceToFollow, and want to add previously not seen continuations for the last
+            // node in the sequence aka 'growth point'.
+            if (!mLastRegisteredEvent.mNextEvents.containsKey(event) || mReproString != null) {
+                // The event was never seen as a continuation for the current node.
+                // Or we are in repro mode, in which case we are not in business of generating
+                // new sequences after we've played back the required sequence.
+                // Register it immediately.
+                registerEventLocked(event);
+            } else {
+                // The event was seen as a continuation for the current node. Postpone it, hoping
+                // that a new event will come from other threads.
+                return createWaitObjectForPostponedEventLocked(event);
+            }
+        } else {
+            // The third phase of the iteration. We are past the growth point and register
+            // everything that comes.
+            registerEventLocked(event);
+            // Register events that may have been postponed while waiting for an :exit event
+            // during the third phase. We don't do this if just registered event is :enter.
+            if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) {
+                registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
+            }
+        }
+        return null;
+    }
+
+    /** Called when there are chances that we just have registered the whole mSequenceToFollow. */
+    private void checkIfCompletedSequenceToFollowLocked() {
+        if (mRegisteredEventCount == mSequenceToFollow.size()) {
+            // We just entered the second phase of the iteration. We have just registered the
+            // whole mSequenceToFollow, and want to add previously not seen continuations for the
+            // last node in the sequence aka 'growth point'. All seen continuations will be
+            // postponed for SHORT_TIMEOUT_MS. At the end of this time period, we'll let them go.
+            scheduleResumeAllEventsLocked();
+
+            // Among the events that were postponed during the first stage, there may be an event
+            // that wasn't seen after the current. If so, register it immediately because this
+            // creates a new sequence.
+            final Set<String> keys = new HashSet<>(mPostponedEvents.keySet());
+            keys.removeAll(mLastRegisteredEvent.mNextEvents.keySet());
+            if (!keys.isEmpty()) {
+                registerPostponedEventLocked(keys.iterator().next());
+            }
+        }
+    }
+
+    private Semaphore createWaitObjectForPostponedEventLocked(String event) {
+        final Semaphore waitObject = new Semaphore(0);
+        assertTrue("Event already postponed: " + event, !mPostponedEvents.containsKey(event));
+        mPostponedEvents.put(event, waitObject);
+        return waitObject;
+    }
+
+    private void waitUntilCanRegister(String event, Semaphore waitObject) {
+        try {
+            assertTrue("Never registered event: " + event,
+                    waitObject.tryAcquire(LONG_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+        } catch (InterruptedException e) {
+            fail("Wait was interrupted");
+        }
+    }
+
+    /** Schedules resuming all postponed events after SHORT_TIMEOUT_MS */
+    private void scheduleResumeAllEventsLocked() {
+        assertTrue(mResumeAllEventsCallback == null);
+        mResumeAllEventsCallback = this::allEventsResumeCallback;
+        POSTPONED_EVENT_RESUME_HANDLER.postDelayed(mResumeAllEventsCallback, SHORT_TIMEOUT_MS);
+    }
+
+    private synchronized void allEventsResumeCallback() {
+        assertTrue("In callback, but callback is not set", mResumeAllEventsCallback != null);
+        mResumeAllEventsCallback = null;
+        registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
+    }
+
+    private void registerPostponedEventsLocked(Collection<String> events) {
+        for (String event : events) {
+            registerPostponedEventLocked(event);
+            if (eventAsEnter(event) != null) {
+                // Once :enter is registered, switch to waiting for :exit to come. Won't register
+                // other postponed events.
+                break;
+            }
+        }
+    }
+
+    private void registerPostponedEventLocked(String event) {
+        mPostponedEvents.remove(event).release();
+        registerEventLocked(event);
+    }
+
+    /**
+     * If the last registered event was XXX:enter, returns XXX, otherwise, null.
+     */
+    private String lastEventAsEnter() {
+        return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1));
+    }
+
+    /**
+     * If the event is XXX:postfix, returns XXX, otherwise, null.
+     */
+    private static String prefixFromPostfixedEvent(String event, String postfix) {
+        final int columnPos = event.indexOf(':');
+        if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) {
+            return event.substring(0, columnPos);
+        }
+        return null;
+    }
+
+    /**
+     * If the event is XXX:enter, returns XXX, otherwise, null.
+     */
+    private static String eventAsEnter(String event) {
+        return prefixFromPostfixedEvent(event, ENTER_POSTFIX);
+    }
+
+    /**
+     * If the event is XXX:exit, returns XXX, otherwise, null.
+     */
+    private static String eventAsExit(String event) {
+        return prefixFromPostfixedEvent(event, EXIT_POSTFIX);
+    }
+
+    private void registerEventLocked(String event) {
+        assertTrue(canRegisterEventNowLocked(event));
+
+        Log.d(TAG, "Actually registering event: " + event);
+        EventNode next = mLastRegisteredEvent.mNextEvents.get(event);
+        if (next == null) {
+            // This event wasn't seen after mLastRegisteredEvent.
+            next = new EventNode();
+            mLastRegisteredEvent.mNextEvents.put(event, next);
+            // The fact that we've added a new event after the previous one means that the
+            // previous event is still a growth point, unless this event is :exit, which means
+            // that the previous event is :enter.
+            mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null;
+        }
+
+        mLastRegisteredEvent = next;
+        mRegisteredEventCount++;
+
+        if (mCurrentSequence.length() > 0) mCurrentSequence.append("|");
+        mCurrentSequence.append(event);
+        Log.d(TAG, "Repro sequence: " + mCurrentSequence);
+    }
+
+    private void runResumeAllEventsCallbackLocked() {
+        if (mResumeAllEventsCallback != null) {
+            POSTPONED_EVENT_RESUME_HANDLER.removeCallbacks(mResumeAllEventsCallback);
+            mResumeAllEventsCallback.run();
+        }
+    }
+
+    private CharSequence dumpStateLocked() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append("Sequence to follow: ");
+        for (String event : mSequenceToFollow) sb.append(" " + event);
+        sb.append(".\n");
+        sb.append("Registered event count: " + mRegisteredEventCount);
+
+        sb.append("\nPostponed events: ");
+        for (String event : mPostponedEvents.keySet()) sb.append(" " + event);
+        sb.append(".");
+
+        sb.append("\nNodes: \n");
+        mRoot.debugDump(sb, 0, "");
+        return sb;
+    }
+
+    public int numberOfLeafNodes() {
+        return mRoot.numberOfLeafNodes();
+    }
+
+    private List<String> generateSequenceToFollowLocked() {
+        ArrayList<String> sequence = new ArrayList<>();
+        mRoot.populatePathToGrowthPoint(sequence);
+        return sequence;
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
new file mode 100644
index 0000000..59f2173
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/RaceConditionReproducerTest.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class RaceConditionReproducerTest {
+    private final static String SOME_VALID_SEQUENCE_3_3 = "B1|A1|A2|B2|A3|B3";
+
+    private static int factorial(int n) {
+        int res = 1;
+        for (int i = 2; i <= n; ++i) res *= i;
+        return res;
+    }
+
+    RaceConditionReproducer eventProcessor;
+
+    @Before
+    public void setup() {
+        eventProcessor = new RaceConditionReproducer();
+    }
+
+    @After
+    public void tearDown() {
+        TraceHelperForTest.cleanup();
+    }
+
+    private void run3_3_TestAction() throws InterruptedException {
+        Thread tb = new Thread(() -> {
+            eventProcessor.onEvent("B1");
+            eventProcessor.onEvent("B2");
+            eventProcessor.onEvent("B3");
+        });
+        tb.start();
+
+        eventProcessor.onEvent("A1");
+        eventProcessor.onEvent("A2");
+        eventProcessor.onEvent("A3");
+
+        tb.join();
+    }
+
+    @Test
+    @Ignore // The test is too long for continuous testing.
+    // 2 threads, 3 events each.
+    public void test3_3() throws Exception {
+        boolean sawTheValidSequence = false;
+
+        for (; ; ) {
+            eventProcessor.startIteration();
+            run3_3_TestAction();
+            final boolean needMoreIterations = eventProcessor.finishIteration();
+
+            sawTheValidSequence = sawTheValidSequence ||
+                    SOME_VALID_SEQUENCE_3_3.equals(eventProcessor.getCurrentSequenceString());
+
+            if (!needMoreIterations) break;
+        }
+
+        assertEquals("Wrong number of leaf nodes",
+                factorial(3 + 3) / (factorial(3) * factorial(3)),
+                eventProcessor.numberOfLeafNodes());
+        assertTrue(sawTheValidSequence);
+    }
+
+    @Test
+    @Ignore // The test is too long for continuous testing.
+    // 2 threads, 3 events, including enter-exit pairs each.
+    public void test3_3_enter_exit() throws Exception {
+        boolean sawTheValidSequence = false;
+
+        for (; ; ) {
+            eventProcessor.startIteration();
+            Thread tb = new Thread(() -> {
+                eventProcessor.onEvent("B1:enter");
+                eventProcessor.onEvent("B1:exit");
+                eventProcessor.onEvent("B2");
+                eventProcessor.onEvent("B3:enter");
+                eventProcessor.onEvent("B3:exit");
+            });
+            tb.start();
+
+            eventProcessor.onEvent("A1");
+            eventProcessor.onEvent("A2:enter");
+            eventProcessor.onEvent("A2:exit");
+            eventProcessor.onEvent("A3:enter");
+            eventProcessor.onEvent("A3:exit");
+
+            tb.join();
+            final boolean needMoreIterations = eventProcessor.finishIteration();
+
+            sawTheValidSequence = sawTheValidSequence ||
+                    "B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit".
+                            equals(eventProcessor.getCurrentSequenceString());
+
+            if (!needMoreIterations) break;
+        }
+
+        assertEquals("Wrong number of leaf nodes",
+                factorial(3 + 3) / (factorial(3) * factorial(3)),
+                eventProcessor.numberOfLeafNodes());
+        assertTrue(sawTheValidSequence);
+    }
+
+    @Test
+    // 2 threads, 3 events each; reproducing a particular event sequence.
+    public void test3_3_ReproMode() throws Exception {
+        eventProcessor = new RaceConditionReproducer(SOME_VALID_SEQUENCE_3_3);
+        eventProcessor.startIteration();
+        run3_3_TestAction();
+        assertTrue(!eventProcessor.finishIteration());
+        assertEquals(SOME_VALID_SEQUENCE_3_3, eventProcessor.getCurrentSequenceString());
+
+        assertEquals("Wrong number of leaf nodes", 1, eventProcessor.numberOfLeafNodes());
+    }
+
+    @Test
+    @Ignore // The test is too long for continuous testing.
+    // 2 threads with 2 events; 1 thread with 1 event.
+    public void test2_1_2() throws Exception {
+        for (; ; ) {
+            eventProcessor.startIteration();
+            Thread tb = new Thread(() -> {
+                eventProcessor.onEvent("B1");
+                eventProcessor.onEvent("B2");
+            });
+            tb.start();
+
+            Thread tc = new Thread(() -> {
+                eventProcessor.onEvent("C1");
+            });
+            tc.start();
+
+            eventProcessor.onEvent("A1");
+            eventProcessor.onEvent("A2");
+
+            tb.join();
+            tc.join();
+
+            if (!eventProcessor.finishIteration()) break;
+        }
+
+        assertEquals("Wrong number of leaf nodes",
+                factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
+                eventProcessor.numberOfLeafNodes());
+    }
+
+    @Test
+    @Ignore // The test is too long for continuous testing.
+    // 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs.
+    public void test2_1_2_enter_exit() throws Exception {
+        for (; ; ) {
+            eventProcessor.startIteration();
+            Thread tb = new Thread(() -> {
+                eventProcessor.onEvent("B1:enter");
+                eventProcessor.onEvent("B1:exit");
+                eventProcessor.onEvent("B2:enter");
+                eventProcessor.onEvent("B2:exit");
+            });
+            tb.start();
+
+            Thread tc = new Thread(() -> {
+                eventProcessor.onEvent("C1:enter");
+                eventProcessor.onEvent("C1:exit");
+            });
+            tc.start();
+
+            eventProcessor.onEvent("A1:enter");
+            eventProcessor.onEvent("A1:exit");
+            eventProcessor.onEvent("A2:enter");
+            eventProcessor.onEvent("A2:exit");
+
+            tb.join();
+            tc.join();
+
+            if (!eventProcessor.finishIteration()) break;
+        }
+
+        assertEquals("Wrong number of leaf nodes",
+                factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
+                eventProcessor.numberOfLeafNodes());
+    }
+}
diff --git a/tests/src/com/android/launcher3/util/TraceHelperForTest.java b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
new file mode 100644
index 0000000..f1c8a67
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/TraceHelperForTest.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import java.util.LinkedList;
+import java.util.function.IntConsumer;
+
+public class TraceHelperForTest extends TraceHelper {
+
+    private static final TraceHelperForTest INSTANCE_FOR_TEST = new TraceHelperForTest();
+
+    private final ThreadLocal<LinkedList<TraceInfo>> mStack =
+            ThreadLocal.withInitial(LinkedList::new);
+
+    private RaceConditionReproducer mRaceConditionReproducer;
+    private IntConsumer mFlagsChangeListener;
+
+    public static void setRaceConditionReproducer(RaceConditionReproducer reproducer) {
+        TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
+        INSTANCE_FOR_TEST.mRaceConditionReproducer = reproducer;
+    }
+
+    public static void cleanup() {
+        INSTANCE_FOR_TEST.mRaceConditionReproducer = null;
+        INSTANCE_FOR_TEST.mFlagsChangeListener = null;
+    }
+
+    public static void setFlagsChangeListener(IntConsumer listener) {
+        TraceHelper.INSTANCE = INSTANCE_FOR_TEST;
+        INSTANCE_FOR_TEST.mFlagsChangeListener = listener;
+    }
+
+    private TraceHelperForTest() { }
+
+    @Override
+    public Object beginSection(String sectionName, int flags) {
+        LinkedList<TraceInfo> stack = mStack.get();
+        TraceInfo info = new TraceInfo(sectionName, flags);
+        stack.add(info);
+
+        if ((flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
+                 && mRaceConditionReproducer != null) {
+            mRaceConditionReproducer.onEvent(RaceConditionReproducer.enterEvt(sectionName));
+        }
+        updateBinderTracking(stack);
+
+        super.beginSection(sectionName, flags);
+        return info;
+    }
+
+    @Override
+    public void endSection(Object token) {
+        LinkedList<TraceInfo> stack = mStack.get();
+        if (stack.size() == 0) {
+            new Throwable().printStackTrace();
+        }
+        TraceInfo info = (TraceInfo) token;
+        stack.remove(info);
+        if ((info.flags & TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS) != 0
+                && mRaceConditionReproducer != null) {
+            mRaceConditionReproducer.onEvent(RaceConditionReproducer.exitEvt(info.sectionName));
+        }
+        updateBinderTracking(stack);
+
+        super.endSection(token);
+    }
+
+    @Override
+    public Object beginFlagsOverride(int flags) {
+        LinkedList<TraceInfo> stack = mStack.get();
+        TraceInfo info = new TraceInfo(null, flags);
+        stack.add(info);
+        updateBinderTracking(stack);
+        super.beginFlagsOverride(flags);
+        return info;
+    }
+
+    @Override
+    public void endFlagsOverride(Object token) {
+        super.endFlagsOverride(token);
+        LinkedList<TraceInfo> stack = mStack.get();
+        TraceInfo info = (TraceInfo) token;
+        stack.remove(info);
+        updateBinderTracking(stack);
+    }
+
+    private void updateBinderTracking(LinkedList<TraceInfo> stack) {
+        if (mFlagsChangeListener != null) {
+            mFlagsChangeListener.accept(stack.stream()
+                    .mapToInt(info -> info.flags).reduce(0, (a, b) -> a | b));
+        }
+    }
+
+    private static class TraceInfo {
+        public final String sectionName;
+        public final int flags;
+
+        TraceInfo(String sectionName, int flags) {
+            this.sectionName = sectionName;
+            this.flags = flags;
+        }
+    }
+}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 667d06e..1111d32 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -375,11 +375,6 @@
                 .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
     }
 
-    public int getOverviewTaskBorderWidth() {
-        return getTestInfo(TestProtocol.REQUEST_GET_OVERVIEW_TASK_BORDER_WIDTH)
-                .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD);
-    }
-
     int getFocusedTaskHeightForTablet() {
         return getTestInfo(TestProtocol.REQUEST_GET_FOCUSED_TASK_HEIGHT_FOR_TABLET).getInt(
                 TestProtocol.TEST_INFO_RESPONSE_FIELD);
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1d25614..39b93b4 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -18,13 +18,7 @@
 
 import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
 
-import static com.android.launcher3.tapl.LauncherInstrumentation.CALLBACK_RUN_POINT.CALLBACK_HOVER_ENTER;
-import static com.android.launcher3.tapl.LauncherInstrumentation.CALLBACK_RUN_POINT.CALLBACK_HOVER_EXIT;
-
-import android.graphics.Point;
 import android.graphics.Rect;
-import android.os.SystemClock;
-import android.view.MotionEvent;
 
 import androidx.annotation.NonNull;
 import androidx.test.uiautomator.By;
@@ -193,35 +187,4 @@
     boolean isTaskSplit() {
         return mLauncher.findObjectInContainer(mTask.getParent(), "bottomright_snapshot") != null;
     }
-
-    /**
-     * Returns this task's visible bounds.
-     */
-    public Rect getVisibleBounds() {
-        return mTask.getVisibleBounds();
-    }
-
-    /**
-     * Emulate the cursor entering and exiting a hover over this task.
-     */
-    public void hoverCursor() {
-        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
-             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
-                     "cursor hover entering task")) {
-            long downTime = SystemClock.uptimeMillis();
-            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
-                    new Point(mTask.getVisibleCenter().x, mTask.getVisibleCenter().y),
-                    null);
-            mLauncher.runCallbackIfActive(CALLBACK_HOVER_ENTER);
-
-            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
-                    "cursor hover exiting task")) {
-                downTime = SystemClock.uptimeMillis();
-                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
-                        new Point(mTask.getVisibleCenter().x, mTask.getVisibleCenter().y),
-                        null);
-                mLauncher.runCallbackIfActive(CALLBACK_HOVER_EXIT);
-            }
-        }
-    }
 }