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..b609511160 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java @@ -20,11 +20,14 @@ 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; 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 +73,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; /** @@ -108,6 +113,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. @@ -125,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); } @@ -189,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; } /** @@ -249,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; @@ -362,12 +391,26 @@ 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) { - int nextViewIndex = 0; - int numViewsAnimated = 0; + /** 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(); + + if (taskbarRecentsLayoutTransition()) { + updateItemsWithLayoutTransition(hotseatItemInfos, recentTasks); + } else { + updateItemsWithoutLayoutTransition(hotseatItemInfos, recentTasks); + } + } + + private void updateItemsWithoutLayoutTransition( + ItemInfo[] hotseatItemInfos, List recentTasks) { + + mNextViewIndex = 0; mAddedDividerForRecents = false; removeView(mAllAppsButtonContainer); @@ -380,12 +423,101 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } removeView(mQsb); - // Add Hotseat icons. - for (ItemInfo hotseatItemInfo : hotseatItemInfos) { - if (hotseatItemInfo == null) { - continue; - } + updateHotseatItems(hotseatItemInfos); + if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { + addView(mTaskbarDividerContainer, mNextViewIndex++); + mAddedDividerForRecents = true; + } + + updateRecents(recentTasks); + + 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 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; + + for (ItemInfo hotseatItemInfo : hotseatItemInfos) { // Replace any Hotseat views with the appropriate type if it's not already that type. final int expectedLayoutResId; boolean isCollection = false; @@ -401,8 +533,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View hotseatView = null; - while (nextViewIndex < getChildCount()) { - hotseatView = getChildAt(nextViewIndex); + while (isNextViewInSection(ItemInfo.class)) { + hotseatView = getChildAt(mNextViewIndex); // see if the view can be reused if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId) @@ -443,7 +575,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. @@ -459,34 +591,27 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (enableCursorHoverStates()) { setHoverListenerForIcon(hotseatView); } - nextViewIndex++; + mNextViewIndex++; } - if (mTaskbarDividerContainer != null && !recentTasks.isEmpty()) { - addView(mTaskbarDividerContainer, nextViewIndex++); - mAddedDividerForRecents = true; + while (isNextViewInSection(ItemInfo.class)) { + removeAndRecycle(getChildAt(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; 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 = mNextViewIndex + recentTasks.size() + nonTaskIconsToBeAdded; overflowSize = mIdealNumIcons - mMaxNumIcons; if (overflowSize > 0 && mTaskbarOverflowView != null) { - addView(mTaskbarOverflowView, nextViewIndex++); + addView(mTaskbarOverflowView, mNextViewIndex++); } else if (mTaskbarOverflowView != null) { mTaskbarOverflowView.clearItems(); } @@ -496,9 +621,9 @@ 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); + overflownTasks = new ArrayList<>(itemsToAddToOverflow); } // Add Recent/Running icons. @@ -506,10 +631,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); @@ -534,8 +655,8 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar } View recentIcon = null; - while (nextViewIndex < getChildCount()) { - recentIcon = getChildAt(nextViewIndex); + while (isNextViewInSection(GroupTask.class)) { + recentIcon = getChildAt(mNextViewIndex); // see if the view can be reused if ((recentIcon.getSourceLayoutResId() != expectedLayoutResId) @@ -549,15 +670,11 @@ 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); - addView(recentIcon, nextViewIndex, lp); + addView(recentIcon, mNextViewIndex, lp); } if (recentIcon instanceof BubbleTextView btv) { @@ -567,29 +684,17 @@ public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconPar if (enableCursorHoverStates()) { setHoverListenerForIcon(recentIcon); } - nextViewIndex++; + mNextViewIndex++; } - // Remove remaining views - while (nextViewIndex < getChildCount()) { - removeAndRecycle(getChildAt(nextViewIndex)); + while (isNextViewInSection(GroupTask.class)) { + 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 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 new file mode 100644 index 0000000000..0bb404b6bd --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTest.kt @@ -0,0 +1,157 @@ +/* + * 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.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 +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.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.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(ParameterizedAndroidJunit4::class) +class TaskbarViewTest(deviceName: String, flags: FlagsParameterization) { + + 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 + + @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..a6bdbb06e0 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt @@ -0,0 +1,123 @@ +/* + * 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() + } + + /** Verifies that recents from [startIndex] have IDs that match [expectedIds] in order. */ + fun hasRecentsOrder(startIndex: Int, expectedIds: List) { + val actualIds = + view.iconViews.slice(startIndex..