Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 80da467..5a8fba6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.logging.InstanceIdSequence;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
+import com.android.launcher3.taskbar.bubbles.BubbleControllers;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.MultiPropertyFactory;
@@ -86,7 +87,7 @@
     private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener =
             dp -> {
                 onStashedInAppChanged(dp);
-                adjustHotseatForBubbleBar();
+                postAdjustHotseatForBubbleBar();
                 if (mControllers != null && mControllers.taskbarViewController != null) {
                     mControllers.taskbarViewController.onRotationChanged(dp);
                 }
@@ -275,13 +276,16 @@
         }
     }
 
-    private void adjustHotseatForBubbleBar() {
+    private void postAdjustHotseatForBubbleBar() {
         Hotseat hotseat = mLauncher.getHotseat();
-        if (mControllers.bubbleControllers.isEmpty() || hotseat == null) return;
-        boolean hiddenForBubbles =
-                mControllers.bubbleControllers.get().bubbleBarViewController.isHiddenForNoBubbles();
-        if (hiddenForBubbles) return;
-        hotseat.post(() -> adjustHotseatForBubbleBar(/* isBubbleBarVisible= */ true));
+        if (hotseat == null || !isBubbleBarVisible()) return;
+        hotseat.post(() -> adjustHotseatForBubbleBar(isBubbleBarVisible()));
+    }
+
+    private boolean isBubbleBarVisible() {
+        BubbleControllers bubbleControllers = mControllers.bubbleControllers.orElse(null);
+        return bubbleControllers != null
+                && bubbleControllers.bubbleBarViewController.isBubbleBarVisible();
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index db5e0d5..b63cf02 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -339,6 +339,7 @@
             // clear restored state
             mBubbleBarViewController.removeAllBubbles();
             mBubbles.clear();
+            mBubbleBarViewController.showOverflow(update.showOverflow);
         }
 
         BubbleBarBubble bubbleToSelect = null;
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index d00959e..569dd56 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -1142,6 +1142,7 @@
 
     /** Removes all existing bubble views */
     public void removeAllBubbles() {
+        mOverflowAdded = false;
         mBarView.removeAllViews();
     }
 
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
index c81edcd..089706f 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt
@@ -470,7 +470,7 @@
         // Stops requesting focused after first view gets focused.
         recentsView.getTaskViewAt(keyboardTaskFocusIndex).requestFocus() ||
             recentsView.nextTaskView.requestFocus() ||
-            recentsView.getTaskViewAt(0).requestFocus() ||
+            recentsView.getFirstTaskView().requestFocus() ||
             recentsView.requestFocus()
     }
 
diff --git a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
index e334695..20794bf 100644
--- a/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
+++ b/quickstep/src/com/android/quickstep/task/thumbnail/TaskThumbnailView.kt
@@ -52,7 +52,10 @@
     private val recentsCoroutineScope: CoroutineScope = RecentsDependencies.get()
     private val dispatcherProvider: DispatcherProvider = RecentsDependencies.get()
 
-    private lateinit var viewData: TaskThumbnailViewData
+    // This is initialised here and set in onAttachedToWindow because onLayout can be called before
+    // onAttachedToWindow so this property needs to be initialised as it is used below.
+    private var viewData: TaskThumbnailViewData = RecentsDependencies.get(this)
+
     private lateinit var viewModel: TaskThumbnailViewModel
 
     private lateinit var viewAttachedScope: CoroutineScope
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a43c686..4660c51 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -246,13 +246,11 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -854,72 +852,6 @@
 
     private int mTaskViewCount = 0;
 
-    private final TaskViewsIterable mTaskViewsIterable = new TaskViewsIterable();
-
-    public class TaskViewsIterable implements Iterable<TaskView> {
-
-        /**
-         * Iterates TaskViews when its index inside the RecentsView is needed.
-         */
-        public void forEachWithIndexInParent(BiConsumer<TaskView, Integer> consumer) {
-            int childCount = getChildCount();
-            for (int index = 0; index < childCount; index++) {
-                if (getChildAt(index) instanceof TaskView taskView) {
-                    consumer.accept(taskView, index);
-                }
-            }
-        }
-
-        @Override
-        public TaskViewsIterator iterator() {
-            return new TaskViewsIterator();
-        }
-    }
-
-    // An Iterator to iterate all the current TaskViews inside the RecentsView.
-    public class TaskViewsIterator implements Iterator<TaskView> {
-        // Refers to the index of the `TaskView` that will be returned when `next()` is called.
-        private int mNextIndex = 0;
-
-        // The "limit" of this iterator. This is the number of children of the RecentsView when
-        // the iterator was created. Adding & removing elements will invalidate the iteration
-        // anyway (and cause next() to throw) so saving this value will guarantee that the
-        // value of hasNext() remains stable and won't flap between true and false when elements
-        // are added and removed from the RecentsView.
-        private final int mLimit = getChildCount();
-
-        TaskViewsIterator() {
-            advanceIfNeeded();
-        }
-
-        @Override
-        public boolean hasNext() {
-            return mNextIndex < mLimit && mNextIndex < getChildCount();
-        }
-
-        @Override
-        public TaskView next() {
-            if (!hasNext()) {
-                throw new IndexOutOfBoundsException(
-                        String.format("mNextIndex: %d, child count: %d", mNextIndex,
-                                getChildCount()));
-            }
-            TaskView taskView = requireTaskViewAt(mNextIndex);
-            mNextIndex++;
-            advanceIfNeeded();
-            return taskView;
-        }
-
-        // Advances `mNextIndex` until it either points to a `TaskView` or to the end of the
-        // Iterator.
-        private void advanceIfNeeded() {
-            while (mNextIndex < mLimit && mNextIndex < getChildCount() && !(getChildAt(
-                    mNextIndex) instanceof TaskView)) {
-                mNextIndex++;
-            }
-        }
-    }
-
     @Nullable
     public TaskView getFirstTaskView() {
         return mUtils.getFirstTaskView();
@@ -2569,7 +2501,7 @@
 
         List<Integer> visibleTaskIds = new ArrayList<>();
         // Update the task data for the in/visible children
-        getTaskViews().forEachWithIndexInParent((taskView, index) -> {
+        getTaskViews().forEachWithIndexInParent((index, taskView) -> {
             List<TaskContainer> containers = taskView.getTaskContainers();
             if (containers.isEmpty()) {
                 return;
@@ -3237,7 +3169,7 @@
      * Skips rebalance.
      */
     private void updateGridProperties() {
-        updateGridProperties(Integer.MAX_VALUE);
+        updateGridProperties(null);
     }
 
     /**
@@ -3247,10 +3179,10 @@
      * This method only calculates the potential position and depends on {@link #setGridProgress} to
      * apply the actual scaling and translation.
      *
-     * @param startRebalanceAfter which view index to start rebalancing from. Use Integer.MAX_VALUE
-     *                            to skip rebalance
+     * @param lastVisibleTaskViewDuringDismiss which TaskView to start rebalancing from. Use
+     *                                         `null` to skip rebalance.
      */
-    private void updateGridProperties(int startRebalanceAfter) {
+    private void updateGridProperties(TaskView lastVisibleTaskViewDuringDismiss) {
         if (!hasTaskViews()) {
             return;
         }
@@ -3263,19 +3195,10 @@
         float topAccumulatedTranslationX = 0;
         float bottomAccumulatedTranslationX = 0;
 
-        // Contains whether the child index is in top or bottom of grid (for non-focused task)
-        // Different from mTopRowIdSet, which contains the taskViewId of what task is in top row
-        IntSet topSet = new IntSet();
-        IntSet bottomSet = new IntSet();
-
-        final int taskCount = getTaskViewCount();
-        // Horizontal grid translation for each task
-        float[] gridTranslations = new float[taskCount];
+        // Horizontal grid translation for each task.
+        Map<TaskView, Float> gridTranslations = new HashMap<>();
 
         TaskView lastLargeTaskView = mUtils.getLastLargeTaskView();
-        int lastLargeTaskIndex =
-                (lastLargeTaskView == null) ? Integer.MAX_VALUE : indexOfChild(lastLargeTaskView);
-        Set<Integer> largeTasksIndices = new HashSet<>();
         int focusedTaskShift = 0;
         int largeTaskWidthAndSpacing = 0;
         int snappedTaskRowWidth = 0;
@@ -3288,29 +3211,46 @@
         if (!mAnyTaskHasBeenDismissed) {
             mTopRowIdSet.clear();
         }
-        for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = requireTaskViewAt(i);
+
+        // Consecutive task views in the top row or bottom row, which means another one set will
+        // be cleared up while starting to add TaskViews to one of them. Also means only one of
+        // them can be non-empty at most.
+        Set<TaskView> lastTopTaskViews = new HashSet<>();
+        Set<TaskView> lastBottomTaskViews = new HashSet<>();
+
+        int largeTasksCount = 0;
+        // True if the last large TaskView has been visited during the TaskViews iteration.
+        boolean encounteredLastLargeTaskView = false;
+        // True if the highest index visible TaskView has been visited during the TaskViews
+        // iteration.
+        boolean encounteredLastVisibleTaskView = false;
+        for (TaskView taskView : getTaskViews()) {
+            if (taskView == lastLargeTaskView) {
+                encounteredLastLargeTaskView = true;
+            }
+            if (taskView == lastVisibleTaskViewDuringDismiss) {
+                encounteredLastVisibleTaskView = true;
+            }
+            float gridTranslation = 0f;
             int taskWidthAndSpacing = taskView.getLayoutParams().width + mPageSpacing;
             // Evenly distribute tasks between rows unless rearranging due to task dismissal, in
             // which case keep tasks in their respective rows. For the running task, don't join
             // the grid.
-            boolean isLargeTile = taskView.isLargeTile();
-
-            if (isLargeTile) {
+            if (taskView.isLargeTile()) {
+                largeTasksCount++;
                 // DesktopTaskView`s are hidden during split select state, so we shouldn't count
                 // them when calculating row width.
                 if (!(taskView instanceof DesktopTaskView && isSplitSelectionActive())) {
                     topRowWidth += taskWidthAndSpacing;
                     bottomRowWidth += taskWidthAndSpacing;
                 }
-                gridTranslations[i] += focusedTaskShift;
-                gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
+                gridTranslation += focusedTaskShift;
+                gridTranslation += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
 
                 // Center view vertically in case it's from different orientation.
                 taskView.setGridTranslationY((mLastComputedTaskSize.height() + taskTopMargin
                         - taskView.getLayoutParams().height) / 2f);
 
-                largeTasksIndices.add(i);
                 largeTaskWidthAndSpacing = taskWidthAndSpacing;
 
                 if (taskView == snappedTaskView) {
@@ -3318,9 +3258,9 @@
                     snappedTaskRowWidth = taskWidthAndSpacing;
                 }
             } else {
-                if (i > lastLargeTaskIndex) {
+                if (encounteredLastLargeTaskView) {
                     // For tasks after the last large task, shift by large task's width and spacing.
-                    gridTranslations[i] +=
+                    gridTranslation +=
                             mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
                 } else {
                     // For task before the focused task, accumulate the width and spacing to
@@ -3329,10 +3269,10 @@
                 }
                 int taskViewId = taskView.getTaskViewId();
 
-                // Rebalance the grid starting after a certain index
                 boolean isTopRow;
                 if (mAnyTaskHasBeenDismissed) {
-                    if (i > startRebalanceAfter) {
+                    // Rebalance the grid starting after a certain index.
+                    if (encounteredLastVisibleTaskView) {
                         mTopRowIdSet.remove(taskViewId);
                         isTopRow = topRowWidth <= bottomRowWidth;
                     } else {
@@ -3349,47 +3289,43 @@
                     } else {
                         topRowWidth += taskWidthAndSpacing;
                     }
-                    topSet.add(i);
                     mTopRowIdSet.add(taskViewId);
-
                     taskView.setGridTranslationY(mTaskGridVerticalDiff);
 
                     // Move horizontally into empty space.
                     float widthOffset = 0;
-                    for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
-                        if (largeTasksIndices.contains(j)) {
-                            continue;
-                        }
-                        widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+                    for (TaskView bottomTaskView : lastBottomTaskViews) {
+                        widthOffset += bottomTaskView.getLayoutParams().width + mPageSpacing;
                     }
 
                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
-                    gridTranslations[i] += topAccumulatedTranslationX + currentTaskTranslationX;
+                    gridTranslation += topAccumulatedTranslationX + currentTaskTranslationX;
                     topAccumulatedTranslationX += currentTaskTranslationX;
+                    lastTopTaskViews.add(taskView);
+                    lastBottomTaskViews.clear();
                 } else {
                     bottomRowWidth += taskWidthAndSpacing;
-                    bottomSet.add(i);
 
                     // Move into bottom row.
                     taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff);
 
                     // Move horizontally into empty space.
                     float widthOffset = 0;
-                    for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
-                        if (largeTasksIndices.contains(j)) {
-                            continue;
-                        }
-                        widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
+                    for (TaskView topTaskView : lastTopTaskViews) {
+                        widthOffset += topTaskView.getLayoutParams().width + mPageSpacing;
                     }
 
                     float currentTaskTranslationX = mIsRtl ? widthOffset : -widthOffset;
-                    gridTranslations[i] += bottomAccumulatedTranslationX + currentTaskTranslationX;
+                    gridTranslation += bottomAccumulatedTranslationX + currentTaskTranslationX;
                     bottomAccumulatedTranslationX += currentTaskTranslationX;
+                    lastBottomTaskViews.add(taskView);
+                    lastTopTaskViews.clear();
                 }
                 if (taskView == snappedTaskView) {
                     snappedTaskRowWidth = isTopRow ? topRowWidth : bottomRowWidth;
                 }
             }
+            gridTranslations.put(taskView, gridTranslation);
         }
 
         // We need to maintain snapped task's page scroll invariant between quick switch and
@@ -3400,22 +3336,22 @@
         if (snappedTaskView != null) {
             snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment(
                     /*gridEnabled=*/false);
-            snappedTaskGridTranslationX = gridTranslations[snappedPage];
+            snappedTaskGridTranslationX = gridTranslations.get(snappedTaskView);
         }
 
         // Use the accumulated translation of the row containing the last task.
-        float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
+        float clearAllAccumulatedTranslation = !lastTopTaskViews.isEmpty()
                 ? topAccumulatedTranslationX : bottomAccumulatedTranslationX;
 
         // If the last task is on the shorter row, ClearAllButton will embed into the shorter row
         // which is not what we want. Compensate the width difference of the 2 rows in that case.
         float shorterRowCompensation = 0;
         if (topRowWidth <= bottomRowWidth) {
-            if (topSet.contains(taskCount - 1)) {
+            if (!lastTopTaskViews.isEmpty()) {
                 shorterRowCompensation = bottomRowWidth - topRowWidth;
             }
         } else {
-            if (bottomSet.contains(taskCount - 1)) {
+            if (!lastBottomTaskViews.isEmpty()) {
                 shorterRowCompensation = topRowWidth - bottomRowWidth;
             }
         }
@@ -3431,8 +3367,7 @@
         // for ClearAllButton translation. The space at the left side of the large task will be
         // empty and it should be move ClearAllButton further away as well.
         // TODO(b/359573248): Validate the translation for ClearAllButton for grid only.
-        boolean hasOnlyLargeTasks = taskCount == largeTasksIndices.size();
-        if (enableLargeDesktopWindowingTile() && hasOnlyLargeTasks) {
+        if (enableLargeDesktopWindowingTile() && largeTasksCount == getTaskViewCount()) {
             longRowWidth = largeTaskWidthAndSpacing;
         }
 
@@ -3456,7 +3391,7 @@
         float clearAllTotalTranslationX =
                 clearAllAccumulatedTranslation + clearAllShorterRowCompensation
                         + clearAllShortTotalWidthTranslation + snappedTaskNonGridScrollAdjustment;
-        if (!largeTasksIndices.isEmpty()) {
+        if (largeTasksCount > 0) {
             // Shift by focused task's width and spacing if a task is focused.
             clearAllTotalTranslationX +=
                     mIsRtl ? largeTaskWidthAndSpacing : -largeTaskWidthAndSpacing;
@@ -3480,10 +3415,10 @@
             }
         }
 
-        for (int i = 0; i < taskCount; i++) {
-            TaskView taskView = requireTaskViewAt(i);
-            taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
-                    + snappedTaskNonGridScrollAdjustment);
+        for (TaskView taskView : getTaskViews()) {
+            taskView.setGridTranslationX(
+                    gridTranslations.get(taskView) - snappedTaskGridTranslationX
+                            + snappedTaskNonGridScrollAdjustment);
         }
 
         final TaskView runningTask = getRunningTaskView();
@@ -4222,7 +4157,8 @@
                             // Rebalance tasks in the grid
                             int highestVisibleTaskIndex = getHighestVisibleTaskIndex();
                             if (highestVisibleTaskIndex < Integer.MAX_VALUE) {
-                                TaskView taskView = requireTaskViewAt(highestVisibleTaskIndex);
+                                final TaskView taskView = requireTaskViewAt(
+                                        highestVisibleTaskIndex);
 
                                 boolean shouldRebalance;
                                 int screenStart = getPagedOrientationHandler().getPrimaryScroll(
@@ -4250,7 +4186,7 @@
                                 }
 
                                 if (shouldRebalance) {
-                                    updateGridProperties(highestVisibleTaskIndex);
+                                    updateGridProperties(taskView);
                                     updateScrollSynchronously();
                                 }
                             }
@@ -4805,8 +4741,8 @@
     /**
      * Returns iterable [TaskView] children.
      */
-    public TaskViewsIterable getTaskViews() {
-        return mTaskViewsIterable;
+    public RecentsViewUtils.TaskViewsIterable getTaskViews() {
+        return mUtils.getTaskViews();
     }
 
     public void setOnEmptyMessageUpdatedListener(OnEmptyMessageUpdatedListener listener) {
@@ -5239,7 +5175,7 @@
         SplitAnimationTimings timings = AnimUtils.getDeviceOverviewToSplitTimings(
                 mContainer.getDeviceProfile().isTablet);
         if (enableLargeDesktopWindowingTile()) {
-            getTaskViews().forEachWithIndexInParent((taskView, index) -> {
+            getTaskViews().forEachWithIndexInParent((index, taskView) -> {
                 if (taskView instanceof DesktopTaskView) {
                     // Setting pivot to scale down from screen centre.
                     if (isTaskViewVisible(taskView)) {
@@ -6188,7 +6124,7 @@
         }
 
         int lastTaskScroll = getLastTaskScroll(clearAllScroll, clearAllWidth);
-        getTaskViews().forEachWithIndexInParent((taskView, index) -> {
+        getTaskViews().forEachWithIndexInParent((index, taskView) -> {
             float scrollDiff = taskView.getScrollAdjustment(showAsGrid);
             int pageScroll = newPageScrolls[index] + Math.round(scrollDiff);
             if ((mIsRtl && pageScroll < lastTaskScroll)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index ccf22ce..3ac3349 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -22,12 +22,15 @@
 import com.android.quickstep.util.GroupTask
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
+import java.util.function.BiConsumer
 
 /**
  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
  * RecentsView to facilitate the implementation of unit tests.
  */
 class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
+    val taskViews = TaskViewsIterable(recentsView)
+
     /** Takes a screenshot of all [taskView] and return map of taskId to the screenshot */
     fun screenshotTasks(taskView: TaskView): Map<Int, ThumbnailData> {
         val recentsAnimationController = recentsView.recentsAnimationController ?: return emptyMap()
@@ -47,27 +50,37 @@
         return otherTasks + desktopTasks
     }
 
+    class TaskViewsIterable(val recentsView: RecentsView<*, *>) : Iterable<TaskView> {
+        /** Iterates TaskViews when its index inside the RecentsView is needed. */
+        fun forEachWithIndexInParent(consumer: BiConsumer<Int, TaskView>) {
+            recentsView.children.forEachIndexed { index, child ->
+                (child as? TaskView)?.let { consumer.accept(index, it) }
+            }
+        }
+
+        override fun iterator(): Iterator<TaskView> =
+            recentsView.children.mapNotNull { it as? TaskView }.iterator()
+    }
+
     /** Counts [TaskView]s that are [DesktopTaskView] instances. */
-    fun getDesktopTaskViewCount(): Int = recentsView.taskViews.count { it is DesktopTaskView }
+    fun getDesktopTaskViewCount(): Int = taskViews.count { it is DesktopTaskView }
 
     /** Returns a list of all large TaskView Ids from [TaskView]s */
-    fun getLargeTaskViewIds(): List<Int> =
-        recentsView.taskViews.filter { it.isLargeTile }.map { it.taskViewId }
+    fun getLargeTaskViewIds(): List<Int> = taskViews.filter { it.isLargeTile }.map { it.taskViewId }
 
     /** Counts [TaskView]s that are large tiles. */
-    fun getLargeTileCount(): Int = recentsView.taskViews.count { it.isLargeTile }
+    fun getLargeTileCount(): Int = taskViews.count { it.isLargeTile }
 
     /** Returns the first TaskView that should be displayed as a large tile. */
     fun getFirstLargeTaskView(): TaskView? =
-        recentsView.taskViews.firstOrNull {
+        taskViews.firstOrNull {
             it.isLargeTile && !(recentsView.isSplitSelectionActive && it is DesktopTaskView)
         }
 
     /** Returns the expected focus task. */
     fun getExpectedFocusedTask(): TaskView? =
-        if (enableLargeDesktopWindowingTile())
-            recentsView.taskViews.firstOrNull { it !is DesktopTaskView }
-        else recentsView.taskViews.firstOrNull()
+        if (enableLargeDesktopWindowingTile()) taskViews.firstOrNull { it !is DesktopTaskView }
+        else taskViews.firstOrNull()
 
     /**
      * Returns the [TaskView] that should be the current page during task binding, in the following
@@ -81,20 +94,20 @@
     fun getExpectedCurrentTask(runningTaskView: TaskView?, focusedTaskView: TaskView?): TaskView? =
         runningTaskView
             ?: focusedTaskView
-            ?: recentsView.taskViews.firstOrNull { it !is DesktopTaskView }
-            ?: recentsView.taskViews.lastOrNull()
+            ?: taskViews.firstOrNull { it !is DesktopTaskView }
+            ?: taskViews.lastOrNull()
 
     /** Returns the first TaskView if it exists, or null otherwise. */
-    fun getFirstTaskView(): TaskView? = recentsView.taskViews.firstOrNull()
+    fun getFirstTaskView(): TaskView? = taskViews.firstOrNull()
 
     /** Returns the last TaskView if it exists, or null otherwise. */
-    fun getLastTaskView(): TaskView? = recentsView.taskViews.lastOrNull()
+    fun getLastTaskView(): TaskView? = taskViews.lastOrNull()
 
     /** Returns the first TaskView that is not large */
-    fun getFirstSmallTaskView(): TaskView? = recentsView.taskViews.firstOrNull { !it.isLargeTile }
+    fun getFirstSmallTaskView(): TaskView? = taskViews.firstOrNull { !it.isLargeTile }
 
     /** Returns the last TaskView that should be displayed as a large tile. */
-    fun getLastLargeTaskView(): TaskView? = recentsView.taskViews.lastOrNull { it.isLargeTile }
+    fun getLastLargeTaskView(): TaskView? = taskViews.lastOrNull { it.isLargeTile }
 
     /**
      * Gets the list of accessibility children. Currently all the children of RecentsViews are
@@ -108,23 +121,23 @@
         nonRunningTaskCarouselHidden: Boolean,
         runningTaskView: TaskView? = recentsView.runningTaskView,
     ): TaskView? =
-        recentsView.taskViews.firstOrNull {
+        taskViews.firstOrNull {
             it.isVisibleInCarousel(runningTaskView, nonRunningTaskCarouselHidden)
         }
 
     /** Returns the last [TaskView], with some tasks possibly hidden in the carousel. */
     fun getLastTaskViewInCarousel(nonRunningTaskCarouselHidden: Boolean): TaskView? =
-        recentsView.taskViews.lastOrNull {
+        taskViews.lastOrNull {
             it.isVisibleInCarousel(recentsView.runningTaskView, nonRunningTaskCarouselHidden)
         }
 
     /** Returns if any small tasks are fully visible */
     fun isAnySmallTaskFullyVisible(): Boolean =
-        recentsView.taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) }
+        taskViews.any { !it.isLargeTile && recentsView.isTaskViewFullyVisible(it) }
 
     /** Apply attachAlpha to all [TaskView] accordingly to different conditions. */
     fun applyAttachAlpha(nonRunningTaskCarouselHidden: Boolean) {
-        recentsView.taskViews.forEach { taskView ->
+        taskViews.forEach { taskView ->
             taskView.attachAlpha =
                 if (taskView == recentsView.runningTaskView) {
                     RUNNING_TASK_ATTACH_ALPHA.get(recentsView)
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
index 5471072..2f773b3 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -245,6 +245,10 @@
             animator.onStashStateChangingWhileAnimating()
         }
 
+        // The physics animation test util posts the cancellation to the looper thread, so we have
+        // to wait again and let it finish.
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
         // verify that the hide animation was canceled
         assertThat(animatorScheduler.delayedBlock).isNull()
         assertThat(animator.isAnimating).isFalse()
@@ -1296,7 +1300,7 @@
             animator.animateBubbleInForStashed(updatedBubble, isExpanding = false)
         }
 
-        // since animation was interrupted there shouldn`t be additional calls to adjust window
+        // since animation was interrupted there shouldn't be additional calls to adjust window
         assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1)
 
         InstrumentationRegistry.getInstrumentation().runOnMainSync {}
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index 0e2c19a..a19d13a 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -46,6 +46,8 @@
             android:layout_width="@dimen/rounded_button_width"
             android:layout_height="@dimen/rounded_button_width"
             android:layout_marginTop="@dimen/work_edu_card_button_margin_top"
+            android:clickable="true"
+            android:contentDescription="@string/accessibility_close"
             android:gravity="center"
             android:background="@drawable/inset_rounded_action_button">
             <ImageButton
@@ -54,7 +56,7 @@
                 android:clickable="false"
                 android:scaleType="centerInside"
                 android:layout_gravity="center"
-                android:contentDescription="@string/accessibility_close"
+                android:importantForAccessibility="no"
                 android:background="@android:color/transparent"
                 android:src="@drawable/ic_close_work_edu" />
         </FrameLayout>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index c37b56c..b38efc2 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1569,7 +1569,6 @@
         if (!enableAddAppWidgetViaConfigActivityV2() || hostView.getParent() == null) {
             mWorkspace.addInScreen(hostView, launcherInfo);
         }
-        announceForAccessibility(R.string.item_added_to_workspace);
 
         // Show the widget resize frame.
         if (hostView instanceof LauncherAppWidgetHostView) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 69a5a83..a064c88 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -3519,9 +3519,8 @@
 
     @Override
     protected boolean canAnnouncePageDescription() {
-        // Disable announcements while overscrolling potentially to overlay screen because if we end
-        // up on the overlay screen, it will take care of announcing itself.
-        return Float.compare(mOverlayProgress, 0f) == 0;
+        // b/383247157: Disable disruptive home screen page announcement
+        return false;
     }
 
     @Override
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index ba6ed66..81d6631 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -305,7 +305,6 @@
                 info.spanX, info.spanY);
         host.requestLayout();
         mContext.getModelWriter().updateItemInDatabase(info);
-        announceConfirmation(mContext.getString(R.string.widget_resized, info.spanX, info.spanY));
         return true;
     }
 
diff --git a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
index 84d6a6f..65b0662 100644
--- a/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
+++ b/src/com/android/launcher3/accessibility/WorkspaceAccessibilityHelper.java
@@ -114,9 +114,7 @@
         LauncherAccessibilityDelegate.DragInfo dragInfo = mDelegate.getDragInfo();
 
         View child = mView.getChildAt(x, y);
-        if (child == null || child == dragInfo.item) {
-            return mContext.getString(R.string.item_moved);
-        } else {
+        if (child != null && child != dragInfo.item) {
             ItemInfo info = (ItemInfo) child.getTag();
             if (Folder.willAccept(info)) {
                 return mContext.getString(R.string.folder_created);
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 5defef3..e68e3c9 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -680,6 +680,7 @@
         closeOpenFolder(openFolder);
 
         mContent.bindItems(items);
+        mContent.setCanAnnouncePageDescriptionForFolder(true);
         centerAboutIcon();
         mItemsInvalidated = true;
         updateTextViewFocus();
@@ -813,6 +814,7 @@
     @Override
     protected void handleClose(boolean animate) {
         mIsOpen = false;
+        mContent.setCanAnnouncePageDescriptionForFolder(false);
 
         if (!animate && mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
             mCurrentAnimator.cancel();
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index fe26194..8d751e6 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -100,6 +100,8 @@
     // animating or is open.
     private boolean mViewsBound = false;
 
+    private boolean mCanAnnouncePageDescription;
+
     public FolderPagedView(Context context, AttributeSet attrs) {
         this(
                 context,
@@ -170,6 +172,19 @@
         mViewsBound = true;
     }
 
+    void setCanAnnouncePageDescriptionForFolder(boolean canAnnounce) {
+        mCanAnnouncePageDescription = canAnnounce;
+    }
+
+    private boolean canAnnouncePageDescriptionForFolder() {
+        return mCanAnnouncePageDescription;
+    }
+
+    @Override
+    protected boolean canAnnouncePageDescription() {
+        return super.canAnnouncePageDescription() && canAnnouncePageDescriptionForFolder();
+    }
+
     /**
      * Removes all the icons from the folder
      */
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index ab4105c..0ebd69f 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -107,9 +107,9 @@
             IconProvider iconProvider) {
         super(context, dbFileName, MODEL_EXECUTOR.getLooper(),
                 idp.fillResIconDpi, idp.iconBitmapSize, true /* inMemoryCache */, iconProvider);
-        mLauncherApps = mContext.getSystemService(LauncherApps.class);
-        mUserManager = UserCache.INSTANCE.get(mContext);
-        mInstantAppResolver = InstantAppResolver.newInstance(mContext);
+        mLauncherApps = context.getSystemService(LauncherApps.class);
+        mUserManager = UserCache.INSTANCE.get(context);
+        mInstantAppResolver = InstantAppResolver.newInstance(context);
         mWidgetCategoryBitmapInfos = new SparseArray<>();
 
         mCancelledTask = new CancellableTask(() -> null, MAIN_EXECUTOR, c -> { });
@@ -117,7 +117,7 @@
     }
 
     @Override
-    protected long getSerialNumberForUser(@NonNull UserHandle user) {
+    public long getSerialNumberForUser(@NonNull UserHandle user) {
         return mUserManager.getSerialNumberForUser(user);
     }
 
@@ -129,7 +129,7 @@
     @NonNull
     @Override
     public BaseIconFactory getIconFactory() {
-        return LauncherIcons.obtain(mContext);
+        return LauncherIcons.obtain(context);
     }
 
     /**
@@ -151,7 +151,7 @@
         // This will clear all pending updates
         getUpdateHandler();
 
-        mIconDb.close();
+        iconDb.close();
     }
 
     /**
@@ -231,7 +231,7 @@
      * Fill in {@code info} with the icon for {@code si}
      */
     public void getShortcutIcon(ItemInfoWithIcon info, ShortcutInfo si) {
-        getShortcutIcon(info, new CacheableShortcutInfo(si, mContext));
+        getShortcutIcon(info, new CacheableShortcutInfo(si, context));
     }
 
     /**
@@ -279,7 +279,7 @@
         String override = shortcutInfo.getExtras() == null ? null
                 : shortcutInfo.getExtras().getString(EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE);
         if (!TextUtils.isEmpty(override)
-                && InstallSessionHelper.INSTANCE.get(mContext)
+                && InstallSessionHelper.INSTANCE.get(context)
                 .isTrustedPackage(pkg, shortcutInfo.getUserHandle())) {
             pkg = override;
         } else {
@@ -364,11 +364,11 @@
         String componentNameQuery = TextUtils.join(
                 ",", Collections.nCopies(queryParams.length - 1, "?"));
 
-        return mIconDb.query(
-                useLowResIcons ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
-                IconDB.COLUMN_COMPONENT
+        return iconDb.query(
+                useLowResIcons ? COLUMNS_LOW_RES : COLUMNS_HIGH_RES,
+                COLUMN_COMPONENT
                         + " IN ( " + componentNameQuery + " )"
-                        + " AND " + IconDB.COLUMN_USER + " = ?",
+                        + " AND " + COLUMN_USER + " = ?",
                 queryParams);
     }
 
@@ -428,7 +428,7 @@
                 /* user = */ sectionKey.first,
                 /* useLowResIcons = */ sectionKey.second)) {
             // Database title and icon loading
-            int componentNameColumnIndex = c.getColumnIndexOrThrow(IconDB.COLUMN_COMPONENT);
+            int componentNameColumnIndex = c.getColumnIndexOrThrow(COLUMN_COMPONENT);
             while (c.moveToNext()) {
                 ComponentName cn = ComponentName.unflattenFromString(
                         c.getString(componentNameColumnIndex));
@@ -525,9 +525,9 @@
             return;
         }
 
-        WidgetSection widgetSection = WidgetSections.getWidgetSections(mContext)
+        WidgetSection widgetSection = WidgetSections.getWidgetSections(context)
                 .get(infoInOut.widgetCategory);
-        infoInOut.title = mContext.getString(widgetSection.mSectionTitle);
+        infoInOut.title = context.getString(widgetSection.mSectionTitle);
         infoInOut.contentDescription = getUserBadgedLabel(infoInOut.title, infoInOut.user);
         final BitmapInfo cachedBitmap = mWidgetCategoryBitmapInfos.get(infoInOut.widgetCategory);
         if (cachedBitmap != null) {
@@ -535,9 +535,9 @@
             return;
         }
 
-        try (LauncherIcons li = LauncherIcons.obtain(mContext)) {
+        try (LauncherIcons li = LauncherIcons.obtain(context)) {
             final BitmapInfo tempBitmap = li.createBadgedIconBitmap(
-                    mContext.getDrawable(widgetSection.mSectionDrawable),
+                    context.getDrawable(widgetSection.mSectionDrawable),
                     new BaseIconFactory.IconOptions());
             mWidgetCategoryBitmapInfos.put(infoInOut.widgetCategory, tempBitmap);
             infoInOut.bitmap = getBadgedIcon(tempBitmap, infoInOut.user);
@@ -606,7 +606,7 @@
 
     /** Log persistently to FileLog.d for debugging. */
     @Override
-    protected void logdPersistently(String tag, String message, @Nullable Exception e) {
-        FileLog.d(tag, message, e);
+    protected void logPersistently(@NonNull String message, @Nullable Exception e) {
+        FileLog.d(BaseIconCache.TAG, message, e);
     }
 }
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index d84a219..d042b1d 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -290,16 +290,28 @@
             }
             setCurrentPage(requestedPage);
             mPageIndicator.setActiveMarker(requestedPage);
-            mRecommendationPageTitle.setText(mCategoryTitles.get(requestedPage));
+            updatePageTitle(requestedPage);
         }
     }
 
     @Override
+    protected boolean canAnnouncePageDescription() {
+        // Disable announcement as our page title reads out the needed page description
+        return false;
+    }
+
+    private void updatePageTitle(int requestedPage) {
+        String title = mCategoryTitles.get(requestedPage);
+        mRecommendationPageTitle.setText(title);
+        mRecommendationPageTitle.setContentDescription(title + ", " + getCurrentPageDescription());
+    }
+
+    @Override
     protected void notifyPageSwitchListener(int prevPage) {
         if (getPageCount() > 1) {
             // Since the title is outside the paging scroll, we update the title on page switch.
             int nextPage = getNextPage();
-            mRecommendationPageTitle.setText(mCategoryTitles.get(nextPage));
+            updatePageTitle(nextPage);
             mPageSwitchListeners.forEach(listener -> listener.accept(nextPage));
             super.notifyPageSwitchListener(prevPage);
         }
diff --git a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
index bae74c8..9dbed74 100644
--- a/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/icons/IconCacheUpdateHandlerTest.kt
@@ -62,7 +62,11 @@
 
     private var cursor =
         MatrixCursor(
-            arrayOf(IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT, IconDB.COLUMN_FRESHNESS_ID)
+            arrayOf(
+                BaseIconCache.COLUMN_ROWID,
+                BaseIconCache.COLUMN_COMPONENT,
+                BaseIconCache.COLUMN_FRESHNESS_ID,
+            )
         )
 
     private lateinit var updateHandlerUnderTest: IconCacheUpdateHandler
diff --git a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
index 4d181ff..d2229c4 100644
--- a/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
+++ b/tests/src/com/android/launcher3/model/LoaderTaskTest.kt
@@ -124,7 +124,7 @@
         `when`(app.invariantDeviceProfile).thenReturn(idp)
         `when`(launcherBinder.newIdleLock(any())).thenReturn(idleLock)
         `when`(idleLock.awaitLocked(1000)).thenReturn(false)
-        `when`(iconCache.updateHandler).thenReturn(iconCacheUpdateHandler)
+        `when`(iconCache.getUpdateHandler()).thenReturn(iconCacheUpdateHandler)
         `when`(widgetsFilterDataProvider.getDefaultWidgetsFilter()).thenReturn(Predicate { true })
         context.putObject(UserCache.INSTANCE, userCache)