Merge changes I1b67d065,Ib2e7f859 into main

* changes:
  Don't show hover tooltip while an app popup is open
  Migrate TaskbarHoverToolTipControllerTest to use TaskbarUnitTestRule
This commit is contained in:
Tony Wickham
2025-03-18 11:38:05 -07:00
committed by Android (Google) Code Review
6 changed files with 194 additions and 272 deletions
@@ -19,6 +19,7 @@ import static android.view.MotionEvent.ACTION_HOVER_ENTER;
import static android.view.MotionEvent.ACTION_HOVER_EXIT;
import static android.view.View.ALPHA;
import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
import static com.android.launcher3.AbstractFloatingView.TYPE_FOLDER;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
@@ -98,20 +99,18 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener {
@Override
public boolean onHover(View v, MotionEvent event) {
boolean isFolderOpen = AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER);
// If hover leaves a taskbar icon animate the tooltip closed.
if (event.getAction() == ACTION_HOVER_EXIT) {
mHoverToolTipView.close(/* animate= */ false);
mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, false);
} else if (!isFolderOpen && event.getAction() == ACTION_HOVER_ENTER) {
// Do not reveal if any floating views such as folders or edu pop-ups are open.
revealHoverToolTip();
} else if (event.getAction() == ACTION_HOVER_ENTER) {
maybeRevealHoverToolTip();
mActivity.setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS, true);
}
return false;
}
private void revealHoverToolTip() {
private void maybeRevealHoverToolTip() {
if (mHoverView == null || mToolTipText == null) {
return;
}
@@ -122,6 +121,12 @@ public class TaskbarHoverToolTipController implements View.OnHoverListener {
if (mHoverView instanceof FolderIcon && !((FolderIcon) mHoverView).getIconVisible()) {
return;
}
// Do not reveal if floating views such as folders or app pop-ups are open,
// as these views will overlap and not look great.
if (AbstractFloatingView.hasOpenView(mActivity, TYPE_FOLDER | TYPE_ACTION_POPUP)) {
return;
}
Rect iconViewBounds = Utilities.getViewBounds(mHoverView);
mHoverToolTipView.showAtLocation(mToolTipText, iconViewBounds.centerX(),
mTaskbarView.getTop() - mYOffset, /* shouldAutoClose= */ false);
@@ -83,7 +83,7 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba
// Initialized in init.
private TaskbarControllers mControllers;
private boolean mAllowInitialSplitSelection;
private AppInfo[] mAppInfosList;
private AppInfo[] mAppInfosList = AppInfo.EMPTY_ARRAY;
// Saves the ItemInfos in the hotseat without the predicted items.
private SparseArray<ItemInfo> mHotseatInfosList;
private ManageWindowsTaskbarShortcut<BaseTaskbarContext> mManageWindowsTaskbarShortcut;
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2025 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.view.MotionEvent
import android.view.MotionEvent.ACTION_HOVER_ENTER
import android.view.MotionEvent.ACTION_HOVER_EXIT
import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.BubbleTextView
import com.android.launcher3.R
import com.android.launcher3.apppairs.AppPairIcon
import com.android.launcher3.folder.FolderIcon
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatAppPairsItem
import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatFolderItem
import com.android.launcher3.taskbar.TaskbarViewTestUtil.createHotseatWorkspaceItem
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.taskbar.rules.TaskbarWindowSandboxContext
import com.android.launcher3.util.LauncherMultivalentJUnit
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(LauncherMultivalentJUnit::class)
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
class TaskbarHoverToolTipControllerTest {
@get:Rule(order = 0) val context = TaskbarWindowSandboxContext.create()
@get:Rule(order = 1) val taskbarUnitTestRule = TaskbarUnitTestRule(this, context)
@InjectController lateinit var autohideSuspendController: TaskbarAutohideSuspendController
@InjectController lateinit var popupController: TaskbarPopupController
private val taskbarContext: TaskbarActivityContext by taskbarUnitTestRule::activityContext
private lateinit var taskbarView: TaskbarView
private lateinit var iconView: BubbleTextView
private lateinit var appPairIcon: AppPairIcon
private lateinit var folderIcon: FolderIcon
private val isHoverToolTipOpen: Boolean
get() {
// TaskbarHoverToolTip uses ArrowTipView which is type TYPE_ON_BOARD_POPUP.
return AbstractFloatingView.hasOpenView(
taskbarContext,
AbstractFloatingView.TYPE_ON_BOARD_POPUP,
)
}
@Before
fun setup() {
runOnMainSync { taskbarView = taskbarContext.dragLayer.findViewById(R.id.taskbar_view) }
val hotseatItems =
arrayOf(
createHotseatWorkspaceItem(),
createHotseatAppPairsItem(),
createHotseatFolderItem(),
)
runOnMainSync {
taskbarView.updateItems(hotseatItems, emptyList())
iconView =
taskbarView.iconViews.filterIsInstance<BubbleTextView>().first {
it.tag is WorkspaceItemInfo
}
appPairIcon = taskbarView.iconViews.filterIsInstance<AppPairIcon>().first()
folderIcon = taskbarView.iconViews.filterIsInstance<FolderIcon>().first()
}
}
@Test
fun onHover_hoverEnterIcon_revealToolTip_hoverExitIcon_closeToolTip() {
runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) }
assertThat(isHoverToolTipOpen).isTrue()
assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue()
runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_EXIT) }
assertThat(isHoverToolTipOpen).isFalse()
assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse()
}
@Test
fun onHover_hoverEnterFolderIcon_revealToolTip_hoverExitFolderIcon_closeToolTip() {
runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_ENTER) }
assertThat(isHoverToolTipOpen).isTrue()
assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue()
runOnMainSync { folderIcon.dispatchGenericMotionEvent(HOVER_EXIT) }
assertThat(isHoverToolTipOpen).isFalse()
assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse()
}
@Test
fun onHover_hoverEnterAppPair_revealToolTip_hoverExitAppPair_closeToolTip() {
runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_ENTER) }
assertThat(isHoverToolTipOpen).isTrue()
assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isTrue()
runOnMainSync { appPairIcon.dispatchGenericMotionEvent(HOVER_EXIT) }
assertThat(isHoverToolTipOpen).isFalse()
assertThat(autohideSuspendController.isTransientTaskbarStashingSuspended).isFalse()
}
@Test
fun onHover_hoverEnterIconAlignedWithHotseat_noToolTip() {
taskbarContext.setUIController(
object : TaskbarUIController() {
override fun isIconAlignedWithHotseat(): Boolean = true
}
)
runOnMainSync { iconView.dispatchGenericMotionEvent(HOVER_ENTER) }
assertThat(isHoverToolTipOpen).isFalse()
}
@Test
fun onHover_hoverEnterFolderOpen_noToolTip() {
runOnMainSync {
folderIcon.folder.animateOpen()
iconView.dispatchGenericMotionEvent(HOVER_ENTER)
}
assertThat(isHoverToolTipOpen).isFalse()
}
@Test
fun onHover_hoverEnterPopupOpen_noToolTip() {
runOnMainSync {
popupController.showForIcon(iconView)
iconView.dispatchGenericMotionEvent(HOVER_ENTER)
}
assertThat(isHoverToolTipOpen).isFalse()
}
companion object {
private val HOVER_EXIT = MotionEvent.obtain(0, 0, ACTION_HOVER_EXIT, 0f, 0f, 0)
private val HOVER_ENTER = MotionEvent.obtain(0, 0, ACTION_HOVER_ENTER, 0f, 0f, 0)
}
}
@@ -18,8 +18,13 @@ package com.android.launcher3.taskbar
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Bitmap.createBitmap
import android.os.Process
import com.android.launcher3.icons.BitmapInfo
import com.android.launcher3.model.data.AppInfo
import com.android.launcher3.model.data.AppPairInfo
import com.android.launcher3.model.data.FolderInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.taskbar.TaskbarIconType.ALL_APPS
@@ -53,7 +58,27 @@ object TaskbarViewTestUtil {
return WorkspaceItemInfo(
AppInfo(TEST_COMPONENT, "Test App $id", Process.myUserHandle(), Intent())
)
.apply { this.id = id }
.apply {
this.id = id
// Create a placeholder icon so that the test doesn't try to load a high-res icon.
this.bitmap = BitmapInfo.fromBitmap(createBitmap(1, 1, Bitmap.Config.ALPHA_8))
}
}
fun createHotseatAppPairsItem(): AppPairInfo {
return AppPairInfo().apply {
add(createHotseatWorkspaceItem(1))
add(createHotseatWorkspaceItem(2))
}
}
fun createHotseatFolderItem(): FolderInfo {
return FolderInfo().apply {
title = "Test Folder"
add(createHotseatWorkspaceItem(1))
add(createHotseatWorkspaceItem(2))
add(createHotseatWorkspaceItem(3))
}
}
/** Creates a list of fake recent tasks. */
@@ -28,6 +28,7 @@ import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.launcher3.taskbar.TaskbarControllers
import com.android.launcher3.taskbar.TaskbarManager
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks
import com.android.launcher3.taskbar.TaskbarUIController
import com.android.launcher3.taskbar.bubbles.BubbleControllers
import com.android.launcher3.taskbar.rules.TaskbarUnitTestRule.InjectController
import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
@@ -117,6 +118,8 @@ class TaskbarUnitTestRule(
super.recreateTaskbars()
if (currentActivityContext != null) {
injectControllers()
// TODO(b/346394875): we should test a non-default uiController.
activityContext.setUIController(TaskbarUIController.DEFAULT)
controllerInjectionCallback.invoke()
}
}
@@ -1,265 +0,0 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.taskbar;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.Display;
import android.view.MotionEvent;
import androidx.test.filters.SmallTest;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.util.ActivityContextWrapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
/**
* Tests for TaskbarHoverToolTipController.
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class TaskbarHoverToolTipControllerTest extends TaskbarBaseTestCase {
private TaskbarHoverToolTipController mTaskbarHoverToolTipController;
private TestableLooper mTestableLooper;
@Mock private TaskbarView mTaskbarView;
@Mock private MotionEvent mMotionEvent;
@Mock private BubbleTextView mHoverBubbleTextView;
@Mock private FolderIcon mHoverFolderIcon;
@Mock private AppPairIcon mAppPairIcon;
@Mock private Display mDisplay;
@Mock private TaskbarDragLayer mTaskbarDragLayer;
private Folder mSpyFolderView;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
Context context = getApplicationContext();
doAnswer((Answer<Object>) invocation -> context.getSystemService(
(String) invocation.getArgument(0)))
.when(taskbarActivityContext).getSystemService(anyString());
when(taskbarActivityContext.getResources()).thenReturn(context.getResources());
when(taskbarActivityContext.getApplicationInfo()).thenReturn(
context.getApplicationInfo());
when(taskbarActivityContext.getDragLayer()).thenReturn(mTaskbarDragLayer);
when(taskbarActivityContext.getMainLooper()).thenReturn(context.getMainLooper());
when(taskbarActivityContext.getDisplay()).thenReturn(mDisplay);
when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(false);
when(mTaskbarDragLayer.getChildCount()).thenReturn(1);
mSpyFolderView = spy(new Folder(new ActivityContextWrapper(context), null));
when(mTaskbarDragLayer.getChildAt(anyInt())).thenReturn(mSpyFolderView);
doReturn(false).when(mSpyFolderView).isOpen();
when(mHoverBubbleTextView.getText()).thenReturn("tooltip");
doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
((int[]) args[0])[0] = 0;
((int[]) args[0])[1] = 0;
return null;
}).when(mHoverBubbleTextView).getLocationOnScreen(any(int[].class));
when(mHoverBubbleTextView.getWidth()).thenReturn(100);
when(mHoverBubbleTextView.getHeight()).thenReturn(100);
mHoverFolderIcon.mInfo = new FolderInfo();
mHoverFolderIcon.mInfo.title = "tooltip";
doAnswer((Answer<Void>) invocation -> {
Object[] args = invocation.getArguments();
((int[]) args[0])[0] = 0;
((int[]) args[0])[1] = 0;
return null;
}).when(mHoverFolderIcon).getLocationOnScreen(any(int[].class));
when(mHoverFolderIcon.getWidth()).thenReturn(100);
when(mHoverFolderIcon.getHeight()).thenReturn(100);
when(mTaskbarView.getTop()).thenReturn(200);
mTaskbarHoverToolTipController = new TaskbarHoverToolTipController(
taskbarActivityContext, mTaskbarView, mHoverBubbleTextView);
mTestableLooper = TestableLooper.get(this);
}
@Test
public void onHover_hoverEnterIcon_revealToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@Test
public void onHover_hoverExitIcon_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverEnterFolderIcon_revealToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@Test
public void onHover_hoverExitFolderIcon_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverExitFolderOpen_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
doReturn(true).when(mSpyFolderView).isOpen();
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverEnterFolderOpen_noToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
doReturn(true).when(mSpyFolderView).isOpen();
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
assertThat(hoverConsumed).isFalse();
}
@Test
public void onHover_hoverMove_noUpdate() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_MOVE);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverFolderIcon, mMotionEvent);
assertThat(hoverConsumed).isFalse();
}
@Test
public void onHover_hoverEnterAppPair_revealToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
@Test
public void onHover_hoverExitAppPair_closeToolTip() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_EXIT);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mAppPairIcon, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
false);
}
@Test
public void onHover_hoverEnterIconAlignedWithHotseat_noReveal() {
when(mMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(mMotionEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_HOVER_ENTER);
when(taskbarActivityContext.isIconAlignedWithHotseat()).thenReturn(true);
boolean hoverConsumed =
mTaskbarHoverToolTipController.onHover(mHoverBubbleTextView, mMotionEvent);
waitForIdleSync();
assertThat(hoverConsumed).isFalse();
verify(taskbarActivityContext).setAutohideSuspendFlag(FLAG_AUTOHIDE_SUSPEND_HOVERING_ICONS,
true);
}
private void waitForIdleSync() {
mTestableLooper.processAllMessages();
}
}