From 1782af7b6e54ad7a8f678792e03819fb3cf771ab Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Tue, 19 Nov 2024 16:09:16 -0500 Subject: [PATCH 1/3] Filter out unsupported items immediately when updating Taskbar. Fixes an issue where hotseatItems may not reflect the number of hotseat views in Taskbar. Entries in the array can be null (e.g. no predictions but there is space in the hotseat for them). If these are filtered out immediately, hotseatItems.length will correspond to the number of hotseat views. Test: go/testedequals Flag: EXEMPT bugfix Fix: 379704910 Change-Id: I375d8152fccb6df5a9b783842ecf5dcedf6894c1 --- .../taskbar/TaskbarModelCallbacks.java | 2 +- .../launcher3/taskbar/TaskbarView.java | 43 +++--- .../launcher3/taskbar/TaskbarViewTest.kt | 135 ++++++++++++++++++ .../launcher3/taskbar/TaskbarViewTestUtil.kt | 113 +++++++++++++++ 4 files changed, 265 insertions(+), 28 deletions(-) create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java index c20617de01..f905c5f4fc 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java @@ -208,7 +208,7 @@ public class TaskbarModelCallbacks implements private void commitHotseatItemUpdates( ItemInfo[] hotseatItemInfos, List recentTasks) { - mContainer.updateHotseatItems(hotseatItemInfos, recentTasks); + mContainer.updateItems(hotseatItemInfos, recentTasks); mControllers.taskbarViewController.updateIconViewsRunningStates(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 756ab0bd56..a7275ca407 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -25,6 +25,8 @@ import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; +import static java.util.function.Predicate.not; + import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; @@ -70,7 +72,9 @@ import com.android.systemui.shared.recents.model.Task; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Predicate; /** @@ -362,10 +366,15 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar view.setTag(null); } - /** - * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos. - */ - protected void updateHotseatItems(ItemInfo[] hotseatItemInfos, List recentTasks) { + /** Inflates/binds the hotseat items and recent tasks to the view. */ + protected void updateItems(ItemInfo[] hotseatItemInfos, List recentTasks) { + // Filter out unsupported items. + hotseatItemInfos = Arrays.stream(hotseatItemInfos) + .filter(Objects::nonNull) + .toArray(ItemInfo[]::new); + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. + recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList(); + int nextViewIndex = 0; int numViewsAnimated = 0; mAddedDividerForRecents = false; @@ -382,10 +391,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // Add Hotseat icons. for (ItemInfo hotseatItemInfo : hotseatItemInfos) { - if (hotseatItemInfo == null) { - continue; - } - // Replace any Hotseat views with the appropriate type if it's not already that type. final int expectedLayoutResId; boolean isCollection = false; @@ -473,16 +478,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar boolean supportsOverflow = Flags.taskbarOverflow(); int overflowSize = 0; - int numberOfSupportedRecents = 0; if (supportsOverflow) { - for (GroupTask task : recentTasks) { - // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. - if (!task.supportsMultipleTasks()) { - ++numberOfSupportedRecents; - } - } - - mIdealNumIcons = nextViewIndex + numberOfSupportedRecents + nonTaskIconsToBeAdded; + mIdealNumIcons = nextViewIndex + recentTasks.size() + nonTaskIconsToBeAdded; overflowSize = mIdealNumIcons - mMaxNumIcons; if (overflowSize > 0 && mTaskbarOverflowView != null) { @@ -496,7 +493,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // An extra item needs to be added to overflow button to account for the space taken up by // the overflow button. final int itemsToAddToOverflow = - (overflowSize > 0) ? Math.min(overflowSize + 1, numberOfSupportedRecents) : 0; + (overflowSize > 0) ? Math.min(overflowSize + 1, recentTasks.size()) : 0; if (overflowSize > 0) { overflownTasks = new ArrayList(itemsToAddToOverflow); } @@ -506,10 +503,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (mTaskbarOverflowView != null && overflownTasks != null && overflownTasks.size() < itemsToAddToOverflow) { // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. - if (task.supportsMultipleTasks()) { - continue; - } - overflownTasks.add(task.task1); if (overflownTasks.size() == itemsToAddToOverflow) { mTaskbarOverflowView.setItems(overflownTasks); @@ -549,11 +542,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } if (recentIcon == null) { - if (isCollection) { - // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. - continue; - } - + // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. recentIcon = inflate(expectedLayoutResId); LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize); recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt new file mode 100644 index 0000000000..e400a4f2f6 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import com.android.launcher3.R +import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems +import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule +import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl +import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) +class TaskbarViewTest { + + @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + + private lateinit var taskbarView: TaskbarView + + @Before + fun obtainView() { + taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view) + } + + @Test + fun testUpdateItems_noItems_hasOnlyAllApps() { + runOnMainSync { taskbarView.updateItems(emptyArray(), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS) + } + + @Test + fun testUpdateItems_hotseatItems_hasDividerBetweenAllAppsAndHotseat() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlWithHotseatItems_hasDividerBetweenHotseatAndAllApps() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) } + assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_withNullHotseatItem_filtersNullItem() { + runOnMainSync { + taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList()) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, HOTSEAT, HOTSEAT) + } + + @Test + @ForceRtl + fun testUpdateItems_rtlWithNullHotseatItem_filtersNullItem() { + runOnMainSync { + taskbarView.updateItems(arrayOf(*createHotseatItems(2), null), emptyList()) + } + assertThat(taskbarView).hasIconTypes(HOTSEAT, HOTSEAT, DIVIDER, ALL_APPS) + } + + @Test + fun testUpdateItems_recentsItems_hasDividerBetweenAllAppsAndRecents() { + runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, DIVIDER, *RECENT * 4) + } + + @Test + fun testUpdateItems_hotseatItemsAndRecents_hasDividerBetweenHotseatAndRecents() { + runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) } + assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 3, DIVIDER, *RECENT * 2) + } + + @Test + fun testUpdateItems_addHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, *HOTSEAT * 2, DIVIDER, RECENT) + } + + @Test + fun testUpdateItems_removeHotseatItem_updatesHotseat() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(2), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } + + @Test + fun testUpdateItems_addRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, *RECENT * 2) + } + + @Test + fun testUpdateItems_removeRecentsItem_updatesRecents() { + runOnMainSync { + taskbarView.updateItems(createHotseatItems(1), createRecents(2)) + taskbarView.updateItems(createHotseatItems(1), createRecents(1)) + } + assertThat(taskbarView).hasIconTypes(ALL_APPS, HOTSEAT, DIVIDER, RECENT) + } +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt new file mode 100644 index 0000000000..7ecd1c77e9 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.taskbar + +import android.content.ComponentName +import android.content.Intent +import android.os.Process +import com.android.launcher3.model.data.AppInfo +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS +import com.android.launcher3.taskbar.TaskbarIconType.DIVIDER +import com.android.launcher3.taskbar.TaskbarIconType.HOTSEAT +import com.android.launcher3.taskbar.TaskbarIconType.OVERFLOW +import com.android.launcher3.taskbar.TaskbarIconType.RECENT +import com.android.quickstep.util.GroupTask +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat + +/** Common utilities for testing [TaskbarView]. */ +object TaskbarViewTestUtil { + + /** Begins an assertion about a [TaskbarView]. */ + fun assertThat(view: TaskbarView): TaskbarViewSubject { + return assertAbout(::TaskbarViewSubject).that(view) + } + + /** Creates an array of fake hotseat items. */ + fun createHotseatItems(size: Int): Array { + return Array(size) { + WorkspaceItemInfo( + AppInfo(TEST_COMPONENT, "Test App $it", Process.myUserHandle(), Intent()) + ) + .apply { id = it } + } + } + + /** Creates a list of fake recent tasks. */ + fun createRecents(size: Int): List { + return List(size) { + GroupTask( + Task().apply { + key = + TaskKey( + it, + 5, + TEST_INTENT, + TEST_COMPONENT, + Process.myUserHandle().identifier, + System.currentTimeMillis(), + ) + } + ) + } + } +} + +/** A `Truth` [Subject] with extensions for verifying [TaskbarView]. */ +class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: TaskbarView) : + Subject(failureMetadata, view) { + + /** Verifies that the types of icons match [expectedTypes] in order. */ + fun hasIconTypes(vararg expectedTypes: TaskbarIconType) { + val actualTypes = + view.iconViews.map { + when (it) { + view.allAppsButtonContainer -> ALL_APPS + view.taskbarDividerViewContainer -> DIVIDER + view.taskbarOverflowView -> OVERFLOW + else -> + when (it.tag) { + is ItemInfo -> HOTSEAT + is GroupTask -> RECENT + else -> throw IllegalStateException("Unknown type for $it") + } + } + } + assertThat(actualTypes).containsExactly(*expectedTypes).inOrder() + } +} + +/** Types of icons in the [TaskbarView]. */ +enum class TaskbarIconType { + ALL_APPS, + DIVIDER, + HOTSEAT, + RECENT, + OVERFLOW; + + operator fun times(size: Int) = Array(size) { this } +} + +private const val TEST_PACKAGE = "com.android.launcher3.taskbar" +private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "Activity") +private val TEST_INTENT = Intent().apply { `package` = TEST_PACKAGE } From 199f860b709ea2cc362f2a752fe7a03c8a93da4c Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Tue, 19 Nov 2024 18:15:46 -0500 Subject: [PATCH 2/3] Split up hotseat and recents into two methods. To support RTL with recents, will need to support calling updateRecents before updateHotseatItems. Splitting into two methods makes this doable. Test: go/testedequals Flag: EXEMPT refactor Bug: 343521765, 368522274 Change-Id: I8756ea23fff1606ef4ab9f72d18dd4b21f135b25 --- .../launcher3/taskbar/TaskbarView.java | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index a7275ca407..360f5c5891 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -112,6 +112,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // Only non-null when device supports having a Taskbar Overflow button. @Nullable private TaskbarOverflowView mTaskbarOverflowView; + private int mNextViewIndex; + /** * Whether the divider is between Hotseat icons and Recents, * instead of between All Apps button and Hotseat. @@ -375,8 +377,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList(); - int nextViewIndex = 0; - int numViewsAnimated = 0; + mNextViewIndex = 0; mAddedDividerForRecents = false; removeView(mAllAppsButtonContainer); @@ -389,7 +390,40 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } removeView(mQsb); - // Add Hotseat icons. + updateHotseatItems(hotseatItemInfos); + + if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { + addView(mTaskbarDividerContainer, mNextViewIndex++); + mAddedDividerForRecents = true; + } + + updateRecents(recentTasks); + + // Remove remaining views + while (mNextViewIndex < getChildCount()) { + removeAndRecycle(getChildAt(mNextViewIndex)); + } + + addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0); + + // If there are no recent tasks, add divider after All Apps (unless it's the only view). + if (!mAddedDividerForRecents + && mTaskbarDividerContainer != null + && getChildCount() > 1) { + addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1); + } + + + if (mActivityContext.getDeviceProfile().isQsbInline) { + addView(mQsb, mIsRtl ? getChildCount() : 0); + // Always set QSB to invisible after re-adding. + mQsb.setVisibility(View.INVISIBLE); + } + } + + private void updateHotseatItems(ItemInfo[] hotseatItemInfos) { + int numViewsAnimated = 0; + for (ItemInfo hotseatItemInfo : hotseatItemInfos) { // Replace any Hotseat views with the appropriate type if it's not already that type. final int expectedLayoutResId; @@ -406,8 +440,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View hotseatView = null; - while (nextViewIndex < getChildCount()) { - hotseatView = getChildAt(nextViewIndex); + while (mNextViewIndex < getChildCount()) { + hotseatView = getChildAt(mNextViewIndex); // see if the view can be reused if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId) @@ -448,7 +482,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize); hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); - addView(hotseatView, nextViewIndex, lp); + addView(hotseatView, mNextViewIndex, lp); } // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index. @@ -464,14 +498,11 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (enableCursorHoverStates()) { setHoverListenerForIcon(hotseatView); } - nextViewIndex++; - } - - if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { - addView(mTaskbarDividerContainer, nextViewIndex++); - mAddedDividerForRecents = true; + mNextViewIndex++; } + } + private void updateRecents(List recentTasks) { // At this point, the all apps button has not been added as a child view, but needs to be // accounted for when comparing current icon count to max number of icons. int nonTaskIconsToBeAdded = 1; @@ -479,11 +510,11 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar boolean supportsOverflow = Flags.taskbarOverflow(); int overflowSize = 0; if (supportsOverflow) { - mIdealNumIcons = nextViewIndex + recentTasks.size() + nonTaskIconsToBeAdded; + mIdealNumIcons = mNextViewIndex + recentTasks.size() + nonTaskIconsToBeAdded; overflowSize = mIdealNumIcons - mMaxNumIcons; if (overflowSize > 0 && mTaskbarOverflowView != null) { - addView(mTaskbarOverflowView, nextViewIndex++); + addView(mTaskbarOverflowView, mNextViewIndex++); } else if (mTaskbarOverflowView != null) { mTaskbarOverflowView.clearItems(); } @@ -495,7 +526,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar final int itemsToAddToOverflow = (overflowSize > 0) ? Math.min(overflowSize + 1, recentTasks.size()) : 0; if (overflowSize > 0) { - overflownTasks = new ArrayList(itemsToAddToOverflow); + overflownTasks = new ArrayList<>(itemsToAddToOverflow); } // Add Recent/Running icons. @@ -527,8 +558,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View recentIcon = null; - while (nextViewIndex < getChildCount()) { - recentIcon = getChildAt(nextViewIndex); + while (mNextViewIndex < getChildCount()) { + recentIcon = getChildAt(mNextViewIndex); // see if the view can be reused if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId) @@ -546,7 +577,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar recentIcon = inflate(expectedLayoutResId); LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize); recentIcon.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding); - addView(recentIcon, nextViewIndex, lp); + addView(recentIcon, mNextViewIndex, lp); } if (recentIcon instanceof BubbleTextView btv) { @@ -556,28 +587,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (enableCursorHoverStates()) { setHoverListenerForIcon(recentIcon); } - nextViewIndex++; - } - - // Remove remaining views - while (nextViewIndex < getChildCount()) { - removeAndRecycle(getChildAt(nextViewIndex)); - } - - addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0); - - // If there are no recent tasks, add divider after All Apps (unless it's the only view). - if (!mAddedDividerForRecents - && mTaskbarDividerContainer != null - && getChildCount() > 1) { - addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1); - } - - - if (mActivityContext.getDeviceProfile().isQsbInline) { - addView(mQsb, mIsRtl ? getChildCount() : 0); - // Always set QSB to invisible after re-adding. - mQsb.setVisibility(View.INVISIBLE); + mNextViewIndex++; } } From c87f691e0448b818bc06a4ac38049431859ff056 Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Tue, 19 Nov 2024 18:51:14 -0500 Subject: [PATCH 3/3] Prepare for LayoutTransition with RTL support. Only remove divider when it needs to be removed or moved between All Apps and recents locations. Keep All Apps and QSB in Taskbar at all times. LayoutTransition does not work if views are being removed and then immediately readded (what current code branch does). Overflow is not supported yet with the LayoutTransition flag. Test: go/tested-equals Flag: com.android.launcher3.taskbar_recents_layout_transition Bug: 368522274, 343521765 Change-Id: I9ae1d7c52e853106cdf86329be6f221213e7cb86 --- .../launcher3/taskbar/TaskbarView.java | 122 +++++++++++++++-- .../launcher3/taskbar/TaskbarViewTest.kt | 36 ++++- .../launcher3/taskbar/TaskbarViewTestUtil.kt | 10 ++ .../TaskbarViewWithLayoutTransitionTest.kt | 124 ++++++++++++++++++ 4 files changed, 277 insertions(+), 15 deletions(-) create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewWithLayoutTransitionTest.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java index 360f5c5891..b609511160 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -20,6 +20,7 @@ import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_ import static com.android.launcher3.BubbleTextView.DISPLAY_TASKBAR; import static com.android.launcher3.Flags.enableCursorHoverStates; import static com.android.launcher3.Flags.enableRecentsInTaskbar; +import static com.android.launcher3.Flags.taskbarRecentsLayoutTransition; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR; import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER; import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; @@ -131,6 +132,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar private final int mAllAppsButtonTranslationOffset; + private final int mNumStaticViews; + public TaskbarView(@NonNull Context context) { this(context, null); } @@ -195,6 +198,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // TODO: Disable touch events on QSB otherwise it can crash. mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false); + + mNumStaticViews = taskbarRecentsLayoutTransition() ? addStaticViews() : 0; } /** @@ -255,6 +260,24 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar && (mIdealNumIcons > oldMaxNumIcons || mIdealNumIcons > mMaxNumIcons); } + /** + * Pre-adds views that are always children of this view for LayoutTransition support. + *

+ * Normally these views are removed and re-added when updating hotseat and recents. This + * approach does not behave well with LayoutTransition, so we instead need to add them + * initially and avoid removing them during updates. + */ + private int addStaticViews() { + int numStaticViews = 1; + addView(mAllAppsButtonContainer); + if (mActivityContext.getDeviceProfile().isQsbInline) { + addView(mQsb, mIsRtl ? 1 : 0); + mQsb.setVisibility(View.INVISIBLE); + numStaticViews++; + } + return numStaticViews; + } + @Override public void setVisibility(int visibility) { boolean changed = getVisibility() != visibility; @@ -377,6 +400,16 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar // TODO(b/343289567 and b/316004172): support app pairs and desktop mode. recentTasks = recentTasks.stream().filter(not(GroupTask::supportsMultipleTasks)).toList(); + if (taskbarRecentsLayoutTransition()) { + updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks); + } else { + updateItemsWithoutLayoutTransition(hotseatItemInfos, recentTasks); + } + } + + private void updateItemsWithoutLayoutTransition( + ItemInfo[] hotseatItemInfos, List recentTasks) { + mNextViewIndex = 0; mAddedDividerForRecents = false; @@ -399,11 +432,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar updateRecents(recentTasks); - // Remove remaining views - while (mNextViewIndex < getChildCount()) { - removeAndRecycle(getChildAt(mNextViewIndex)); - } - addView(mAllAppsButtonContainer, mIsRtl ? hotseatItemInfos.length : 0); // If there are no recent tasks, add divider after All Apps (unless it's the only view). @@ -413,7 +441,6 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar addView(mTaskbarDividerContainer, mIsRtl ? (getChildCount() - 1) : 1); } - if (mActivityContext.getDeviceProfile().isQsbInline) { addView(mQsb, mIsRtl ? getChildCount() : 0); // Always set QSB to invisible after re-adding. @@ -421,6 +448,72 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } } + private void updateItemsWithLayoutTransition( + ItemInfo[] hotseatItemInfos, List recentTasks) { + + // Skip static views and potential All Apps divider, if they are on the left. + mNextViewIndex = mIsRtl ? 0 : mNumStaticViews; + if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) { + mNextViewIndex++; + } + + // Update left section. + if (mIsRtl) { + updateRecents(recentTasks.reversed()); + } else { + updateHotseatItems(hotseatItemInfos); + } + + // Now at theoretical position for recent apps divider. + updateRecentsDivider(!recentTasks.isEmpty()); + if (getChildAt(mNextViewIndex) == mTaskbarDividerContainer) { + mNextViewIndex++; + } + + // Update right section. + if (mIsRtl) { + updateHotseatItems(hotseatItemInfos); + } else { + updateRecents(recentTasks); + } + + // Recents divider takes priority. + if (!mAddedDividerForRecents) { + updateAllAppsDivider(); + } + } + + private void updateRecentsDivider(boolean hasRecents) { + if (hasRecents && !mAddedDividerForRecents) { + mAddedDividerForRecents = true; + + // Remove possible All Apps divider. + if (getChildAt(mNumStaticViews) == mTaskbarDividerContainer) { + mNextViewIndex--; // All Apps divider on the left. Need to account for removing it. + } + removeView(mTaskbarDividerContainer); + + addView(mTaskbarDividerContainer, mNextViewIndex); + } else if (!hasRecents && mAddedDividerForRecents) { + mAddedDividerForRecents = false; + removeViewAt(mNextViewIndex); + } + } + + private void updateAllAppsDivider() { + final int allAppsDividerIndex = + mIsRtl ? getChildCount() - mNumStaticViews : mNumStaticViews; + if (getChildAt(allAppsDividerIndex) == mTaskbarDividerContainer + && getChildCount() == mNumStaticViews + 1) { + // Only static views with divider so remove divider. + removeView(mTaskbarDividerContainer); + } else if (getChildAt(allAppsDividerIndex) != mTaskbarDividerContainer + && getChildCount() >= mNumStaticViews + 1) { + // Static views with at least one app icon so add divider. + addView(mTaskbarDividerContainer, allAppsDividerIndex); + } + } + private void updateHotseatItems(ItemInfo[] hotseatItemInfos) { int numViewsAnimated = 0; @@ -440,7 +533,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View hotseatView = null; - while (mNextViewIndex < getChildCount()) { + while (isNextViewInSection(ItemInfo.class)) { hotseatView = getChildAt(mNextViewIndex); // see if the view can be reused @@ -500,6 +593,10 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } mNextViewIndex++; } + + while (isNextViewInSection(ItemInfo.class)) { + removeAndRecycle(getChildAt(mNextViewIndex)); + } } private void updateRecents(List recentTasks) { @@ -558,7 +655,7 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View recentIcon = null; - while (mNextViewIndex < getChildCount()) { + while (isNextViewInSection(GroupTask.class)) { recentIcon = getChildAt(mNextViewIndex); // see if the view can be reused @@ -589,6 +686,15 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } mNextViewIndex++; } + + while (isNextViewInSection(GroupTask.class)) { + removeAndRecycle(getChildAt(mNextViewIndex)); + } + } + + private boolean isNextViewInSection(Class tagClass) { + return mNextViewIndex < getChildCount() + && tagClass.isInstance(getChildAt(mNextViewIndex).getTag()); } /** Binds the GroupTask to the BubbleTextView to be ready to present to the user. */ diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt index e400a4f2f6..0bb404b6bd 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt @@ -16,6 +16,10 @@ package com.android.launcher3.taskbar +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf +import android.platform.test.flag.junit.SetFlagsRule +import com.android.launcher3.Flags.FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION import com.android.launcher3.R import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS @@ -25,22 +29,40 @@ import com.android.launcher3.taskbar.TaskbarIconType.RECENT import com.android.launcher3.taskbar.TaskbarViewTestUtil.assertThat import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatItems import com.android.launcher3.taskbar.TaskbarViewTestUtil.createRecents +import com.android.launcher3.taskbar.rules.TaskbarDeviceEmulationRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.ForceRtl import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext -import com.android.launcher3.util.LauncherMultivalentJUnit -import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters -@RunWith(LauncherMultivalentJUnit::class) -@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"]) -class TaskbarViewTest { +@RunWith(ParameterizedAndroidJunit4::class) +class TaskbarViewTest(deviceName: String, flags: FlagsParameterization) { - @get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create() - @get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) + companion object { + @JvmStatic + @Parameters(name = "{0},{1}") + fun getParams(): List> { + val devices = + if (isRunningInRobolectric) { + listOf("pixelFoldable2023", "pixelTablet2023") + } else { + listOf("onDevice") // Unused. + } + val flags = allCombinationsOf(FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION) + return devices.flatMap { d -> flags.map { f -> arrayOf(d, f) } } // Cartesian product. + } + } + + @get:Rule(order = 0) val setFlagsRule = SetFlagsRule(flags) + @get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create() + @get:Rule(order = 2) val deviceEmulationRule = TaskbarDeviceEmulationRule(context, deviceName) + @get:Rule(order = 3) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context) private lateinit var taskbarView: TaskbarView diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt index 7ecd1c77e9..a6bdbb06e0 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt @@ -95,6 +95,16 @@ class TaskbarViewSubject(failureMetadata: FailureMetadata, private val view: Tas } assertThat(actualTypes).containsExactly(*expectedTypes).inOrder() } + + /** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */ + fun hasRecentsOrder(startIndex: Int, expectedIds: List) { + val actualIds = + view.iconViews.slice(startIndex..