Merge changes from topic "taskbar-layout-transition-prepare" into main

* changes:
  Prepare for LayoutTransition with RTL support.
  Split up hotseat and recents into two methods.
  Filter out unsupported items immediately when updating Taskbar.
This commit is contained in:
Brian Isganitis
2024-11-21 19:39:05 +00:00
committed by Android (Google) Code Review
5 changed files with 571 additions and 62 deletions
@@ -208,7 +208,7 @@ public class TaskbarModelCallbacks implements
private void commitHotseatItemUpdates(
ItemInfo[] hotseatItemInfos, List<GroupTask> recentTasks) {
mContainer.updateHotseatItems(hotseatItemInfos, recentTasks);
mContainer.updateItems(hotseatItemInfos, recentTasks);
mControllers.taskbarViewController.updateIconViewsRunningStates();
}
@@ -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.
* <p>
* 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<GroupTask> recentTasks) {
int nextViewIndex = 0;
int numViewsAnimated = 0;
/** Inflates/binds the hotseat items and recent tasks to the view. */
protected void updateItems(ItemInfo[] hotseatItemInfos, List<GroupTask> 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<GroupTask> 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<GroupTask> 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<GroupTask> 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<Task>(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. */
@@ -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<Array<Any>> {
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)
}
}
@@ -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<ItemInfo> {
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<GroupTask> {
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<Int>) {
val actualIds =
view.iconViews.slice(startIndex..<expectedIds.size).map {
assertThat(it.tag).isInstanceOf(GroupTask::class.java)
(it.tag as? GroupTask)?.task1?.key?.id
}
assertThat(actualIds).containsExactlyElementsIn(expectedIds).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 }
@@ -0,0 +1,124 @@
/*
* 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.annotations.EnableFlags
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.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"])
@EnableFlags(FLAG_TASKBAR_RECENTS_LAYOUT_TRANSITION)
class TaskbarViewWithLayoutTransitionTest {
@get:Rule(order = 0) val setFlagsRule = SetFlagsRule()
@get:Rule(order = 1) val context = TaskbarWindowSandboxContext.create()
@get:Rule(order = 2) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
private lateinit var taskbarView: TaskbarView
@Before
fun obtainView() {
taskbarView = taskbarUnitTestRule.activityContext.dragLayer.findViewById(R.id.taskbar_view)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_hotseatItems_hasDividerBetweenHotseatAndAllApps() {
runOnMainSync { taskbarView.updateItems(createHotseatItems(2), emptyList()) }
assertThat(taskbarView).hasIconTypes(*HOTSEAT * 2, DIVIDER, ALL_APPS)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_recentsItems_hasDividerBetweenRecentsAndAllApps() {
runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
assertThat(taskbarView).hasIconTypes(*RECENT * 4, DIVIDER, ALL_APPS)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_recentsItems_recentsAreReversed() {
runOnMainSync { taskbarView.updateItems(emptyArray(), createRecents(4)) }
assertThat(taskbarView).hasRecentsOrder(startIndex = 0, expectedIds = listOf(3, 2, 1, 0))
}
@Test
@ForceRtl
fun testUpdateItems_rtl_hotseatItemsAndRecents_hasDividerBetweenRecentsAndHotseat() {
runOnMainSync { taskbarView.updateItems(createHotseatItems(3), createRecents(2)) }
assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, *HOTSEAT * 3, ALL_APPS)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_addHotseatItem_updatesHotseat() {
runOnMainSync {
taskbarView.updateItems(createHotseatItems(1), createRecents(1))
taskbarView.updateItems(createHotseatItems(2), createRecents(1))
}
assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, *HOTSEAT * 2, ALL_APPS)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_removeHotseatItem_updatesHotseat() {
runOnMainSync {
taskbarView.updateItems(createHotseatItems(2), createRecents(1))
taskbarView.updateItems(createHotseatItems(1), createRecents(1))
}
assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_addRecentsItem_updatesRecents() {
runOnMainSync {
taskbarView.updateItems(createHotseatItems(1), createRecents(1))
taskbarView.updateItems(createHotseatItems(1), createRecents(2))
}
assertThat(taskbarView).hasIconTypes(*RECENT * 2, DIVIDER, HOTSEAT, ALL_APPS)
}
@Test
@ForceRtl
fun testUpdateItems_rtl_removeRecentsItem_updatesRecents() {
runOnMainSync {
taskbarView.updateItems(createHotseatItems(1), createRecents(2))
taskbarView.updateItems(createHotseatItems(1), createRecents(1))
}
assertThat(taskbarView).hasIconTypes(RECENT, DIVIDER, HOTSEAT, ALL_APPS)
}
}