diff options
| author | 2024-10-01 16:05:30 +0000 | |
|---|---|---|
| committer | 2024-10-01 16:05:30 +0000 | |
| commit | 73c6a70eed2cce469c0309f68f7ca71b8b3dcf6c (patch) | |
| tree | 3f3163a4abd8e4081404d4ee45ce3e42af8b1526 | |
| parent | b3f9f391102fc4ce59992f6c9d028025b11417c9 (diff) | |
| parent | 2112cf822b07597dc0b43f37ff0f6364f47bd58c (diff) | |
Merge "Setup onclick listeners on the tooltip to match what we designed" into main
5 files changed, 187 insertions, 5 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 2d0ec3625d28..2c030591e106 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -252,6 +252,7 @@ public abstract class WMShellModule { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, + AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler) { if (DesktopModeStatus.canEnterDesktopMode(context)) { @@ -277,6 +278,7 @@ public abstract class WMShellModule { assistContentRequester, multiInstanceHelper, desktopTasksLimiter, + appHandleEducationController, windowDecorCaptionHandleRepository, desktopActivityOrientationHandler); } @@ -869,8 +871,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, - AppHandleEducationController appHandleEducationController + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional ) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt index ea5c432a17d7..a1dfb6862ad3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -31,6 +31,7 @@ import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatasto import com.android.wm.shell.shared.annotations.ShellBackgroundThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.EducationViewConfig import kotlin.time.Duration.Companion.milliseconds @@ -68,6 +69,9 @@ class AppHandleEducationController( @ShellMainThread private val applicationCoroutineScope: CoroutineScope, @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, ) { + private lateinit var openHandleMenuCallback: (Int) -> Unit + private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit + init { runIfEducationFeatureEnabled { applicationCoroutineScope.launch { @@ -114,6 +118,7 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP, onEducationClickAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } + openHandleMenuCallback(captionState.runningTaskInfo.taskId) }, onDismissAction = { launchWithExceptionHandling { showWindowingImageButtonTooltip() } }, ) @@ -171,6 +176,9 @@ class AppHandleEducationController( DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, onEducationClickAction = { launchWithExceptionHandling { showExitWindowingTooltip() } + toDesktopModeCallback( + captionState.runningTaskInfo.taskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) }, onDismissAction = { launchWithExceptionHandling { showExitWindowingTooltip() } }, ) @@ -216,7 +224,9 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.LEFT, onDismissAction = {}, - onEducationClickAction = {}, + onEducationClickAction = { + openHandleMenuCallback(captionState.runningTaskInfo.taskId) + }, ) windowingEducationViewController.showEducationTooltip( taskId = captionState.runningTaskInfo.taskId, @@ -225,6 +235,20 @@ class AppHandleEducationController( } } + /** + * Setup callbacks for app handle education tooltips. + * + * @param openHandleMenuCallback callback invoked to open app handle menu or app chip menu. + * @param toDesktopModeCallback callback invoked to move task into desktop mode. + */ + fun setAppHandleEducationTooltipCallbacks( + openHandleMenuCallback: (taskId: Int) -> Unit, + toDesktopModeCallback: (taskId: Int, DesktopModeTransitionSource) -> Unit + ) { + this.openHandleMenuCallback = openHandleMenuCallback + this.toDesktopModeCallback = toDesktopModeCallback + } + private inline fun <T> Flow<T>.catchTimeoutAndLog(crossinline block: () -> Unit) = catch { exception -> if (exception is TimeoutCancellationException) block() else throw exception diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 272508f46d33..c34a0bc829c4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -87,6 +87,7 @@ import android.window.WindowContainerTransaction; import android.window.flags.DesktopModeFlags; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.Cuj; @@ -112,6 +113,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; +import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; @@ -134,6 +136,8 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Pair; import kotlin.Unit; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -167,6 +171,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; + private final AppHandleEducationController mAppHandleEducationController; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private boolean mTransitionDragActive; @@ -236,6 +241,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { AssistContentRequester assistContentRequester, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, + AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler) { this( @@ -265,6 +271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new SparseArray<>(), interactionJankMonitor, desktopTasksLimiter, + appHandleEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory()); @@ -298,6 +305,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter, + AppHandleEducationController appHandleEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory) { @@ -329,6 +337,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; + mAppHandleEducationController = appHandleEducationController; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; @@ -362,6 +371,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { shellInit.addInitCallback(this::onInit, this); } + @OptIn(markerClass = ExperimentalCoroutinesApi.class) private void onInit() { mShellController.addKeyguardChangeListener(mDesktopModeKeyguardChangeListener); mShellCommandHandler.addDumpCallback(this::dump, this); @@ -378,6 +388,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } catch (RemoteException e) { Log.e(TAG, "Failed to register window manager callbacks", e); } + if (DesktopModeStatus.canEnterDesktopMode(mContext) + && Flags.enableDesktopWindowingAppHandleEducation()) { + mAppHandleEducationController.setAppHandleEducationTooltipCallbacks( + /* appHandleTooltipClickCallback= */(taskId) -> { + openHandleMenu(taskId); + return Unit.INSTANCE; + }, + /* onToDesktopClickCallback= */(taskId, desktopModeTransitionSource) -> { + onToDesktop(taskId, desktopModeTransitionSource); + return Unit.INSTANCE; + }); + } } @Override @@ -495,6 +517,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.remove(taskInfo.taskId); } + private void openHandleMenu(int taskId) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) + >= MANAGE_WINDOWS_MINIMUM_INSTANCES); + } + private void onMaximizeOrRestore(int taskId, String source) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { @@ -720,8 +748,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { moveTaskToFront(decoration.mTaskInfo); - decoration.createHandleMenu(checkNumberOfOtherInstances(decoration.mTaskInfo) - >= MANAGE_WINDOWS_MINIMUM_INSTANCES); + openHandleMenu(mTaskId); } } else if (id == R.id.maximize_window) { // TODO(b/346441962): move click detection logic into the decor's diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt index aa8f911c4df2..aad31a6d57ba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt @@ -31,6 +31,7 @@ import com.android.wm.shell.desktopmode.education.AppHandleEducationController.C import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_TIMEOUT_MILLIS import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.util.createAppHandleState import com.android.wm.shell.util.createAppHeaderState import com.android.wm.shell.util.createWindowingEducationProto @@ -53,6 +54,7 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -337,6 +339,51 @@ class AppHandleEducationControllerTest : ShellTestCase() { verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() = + testScope.runTest { + // App handle is visible. Should show education tooltip. + setShouldShowAppHandleEducation(true) + val mockOpenHandleMenuCallback: (Int) -> Unit = mock() + val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() + educationController.setAppHandleEducationTooltipCallbacks( + mockOpenHandleMenuCallback, mockToDesktopModeCallback) + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, atLeastOnce()) + .showEducationTooltip(educationConfigCaptor.capture(), any()) + educationConfigCaptor.lastValue.onEducationClickAction.invoke() + + verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() = + testScope.runTest { + // After first tooltip is dismissed, app handle is expanded. Should show second education + // tooltip. + showAndDismissFirstTooltip() + val mockOpenHandleMenuCallback: (Int) -> Unit = mock() + val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() + educationController.setAppHandleEducationTooltipCallbacks( + mockOpenHandleMenuCallback, mockToDesktopModeCallback) + // Simulate app handle expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + // Wait for next tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, atLeastOnce()) + .showEducationTooltip(educationConfigCaptor.capture(), any()) + educationConfigCaptor.lastValue.onEducationClickAction.invoke() + + verify(mockToDesktopModeCallback, times(1)).invoke(any(), any()) + } + private suspend fun TestScope.showAndDismissFirstTooltip() { setShouldShowAppHandleEducation(true) // Simulate app handle visible. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 3051714b5ae8..9aa6a52fd851 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -64,6 +64,7 @@ import android.widget.Toast import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.StaticMockitoSession @@ -89,6 +90,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository +import com.android.wm.shell.desktopmode.education.AppHandleEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource @@ -105,6 +107,7 @@ import java.util.function.Consumer import java.util.function.Supplier import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before @@ -136,6 +139,7 @@ import org.mockito.quality.Strictness * Tests of [DesktopModeWindowDecorViewModel] * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests */ +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper @@ -184,6 +188,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { @Mock private lateinit var mockTaskPositionerFactory: DesktopModeWindowDecorViewModel.TaskPositionerFactory @Mock private lateinit var mockTaskPositioner: TaskPositioner + @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository private lateinit var spyContext: TestableContext @@ -242,6 +247,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { windowDecorByTaskIdSpy, mockInteractionJankMonitor, Optional.of(mockTasksLimiter), + mockAppHandleEducationController, mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory @@ -957,6 +963,83 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { }, eq(mockUserHandle)) } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun testDecor_createWindowDecoration_setsAppHandleEducationTooltipClickCallbacks() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + + shellInit.init() + + verify( + mockAppHandleEducationController, + times(1) + ).setAppHandleEducationTooltipCallbacks(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun testDecor_invokeOpenHandleMenuCallback_openHandleMenu() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + val decor = setUpMockDecorationForTask(task) + val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>() + // Set task as gmail + val gmailPackageName = "com.google.android.gm" + val baseComponent = ComponentName(gmailPackageName, /* class */ "") + task.baseActivity = baseComponent + + onTaskOpening(task) + verify( + mockAppHandleEducationController, + times(1) + ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any()) + openHandleMenuCallbackCaptor.lastValue.invoke(task.taskId) + + verify(decor, times(1)).createHandleMenu(anyBoolean()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun testDecor_openTaskWithFlagDisabled_doNotOpenHandleMenu() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + setUpMockDecorationForTask(task) + val openHandleMenuCallbackCaptor = argumentCaptor<(Int) -> Unit>() + // Set task as gmail + val gmailPackageName = "com.google.android.gm" + val baseComponent = ComponentName(gmailPackageName, /* class */ "") + task.baseActivity = baseComponent + + onTaskOpening(task) + verify( + mockAppHandleEducationController, + never() + ).setAppHandleEducationTooltipCallbacks(openHandleMenuCallbackCaptor.capture(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun testDecor_invokeOnToDesktopCallback_setsAppHandleEducationTooltipClickCallbacks() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM) + setUpMockDecorationsForTasks(task) + onTaskOpening(task) + val onToDesktopCallbackCaptor = argumentCaptor<(Int, DesktopModeTransitionSource) -> Unit>() + + verify( + mockAppHandleEducationController, + times(1) + ).setAppHandleEducationTooltipCallbacks(any(), onToDesktopCallbackCaptor.capture()) + onToDesktopCallbackCaptor.lastValue.invoke( + task.taskId, + DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON + ) + + verify(mockDesktopTasksController, times(1)) + .moveTaskToDesktop(any(), any(), any()) + } + @Test fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() { val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) |