From 2f4ccc63b62215317fc79cf9ac02fee49e82336d Mon Sep 17 00:00:00 2001 From: Graciela Wissen Putri Date: Mon, 14 Oct 2024 10:32:22 +0000 Subject: [PATCH] Add task menu item to move task to external display Call SystemUiProxy.moveToExternalDisplay to move existing Overview task to desktop in external display Bug: 372872848 Test: atest NexusLauncherTests:com.android.quickstep.ExternalDisplaySystemShortcutTest Flag: com.android.window.flags.move_to_external_display_shortcut Change-Id: I096a9839956ab5cab86bd0aaabc625a8587ca42a --- .../res/drawable/ic_external_display.xml | 24 +++ quickstep/res/values/strings.xml | 2 + .../DesktopRecentsTransitionController.kt | 5 + .../ExternalDisplaySystemShortcut.kt | 86 ++++++++ .../com/android/quickstep/SystemUiProxy.java | 11 + .../android/quickstep/TaskOverlayFactory.java | 1 + .../android/quickstep/views/RecentsView.java | 20 ++ .../ExternalDisplaySystemShortcutTest.kt | 193 ++++++++++++++++++ .../launcher3/logging/StatsLogManager.java | 3 + 9 files changed, 345 insertions(+) create mode 100644 quickstep/res/drawable/ic_external_display.xml create mode 100644 quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt create mode 100644 quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt diff --git a/quickstep/res/drawable/ic_external_display.xml b/quickstep/res/drawable/ic_external_display.xml new file mode 100644 index 0000000000..64c183ea12 --- /dev/null +++ b/quickstep/res/drawable/ic_external_display.xml @@ -0,0 +1,24 @@ + + + + diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 008766b51b..91c2a6d776 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -28,6 +28,8 @@ Freeform Desktop + + Move to external display Desktop diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt index 8b3a032137..ac1ffa6029 100644 --- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt +++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt @@ -66,6 +66,11 @@ class DesktopRecentsTransitionController( systemUiProxy.moveToDesktop(taskId, transitionSource) } + /** Move task to external display from recents view */ + fun moveToExternalDisplay(taskId: Int) { + systemUiProxy.moveToExternalDisplay(taskId) + } + private class RemoteDesktopLaunchTransitionRunner( private val desktopTaskView: DesktopTaskView, private val animated: Boolean, diff --git a/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt new file mode 100644 index 0000000000..46c4f365be --- /dev/null +++ b/quickstep/src/com/android/quickstep/ExternalDisplaySystemShortcut.kt @@ -0,0 +1,86 @@ +/* + * 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.quickstep + +import android.view.View +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.R +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.popup.SystemShortcut +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskContainer +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus + +/** A menu item that allows the user to move the current app into external display. */ +class ExternalDisplaySystemShortcut( + container: RecentsViewContainer, + abstractFloatingViewHelper: AbstractFloatingViewHelper, + private val taskContainer: TaskContainer, +) : + SystemShortcut( + R.drawable.ic_external_display, + R.string.recent_task_option_external_display, + container, + taskContainer.itemInfo, + taskContainer.taskView, + abstractFloatingViewHelper, + ) { + override fun onClick(view: View) { + dismissTaskMenuView() + val recentsView = mTarget.getOverviewPanel>() + recentsView.moveTaskToExternalDisplay(taskContainer) { + mTarget.statsLogManager + .logger() + .withItemInfo(taskContainer.itemInfo) + .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + } + + companion object { + @JvmOverloads + /** + * Creates a factory for creating move task to external display system shortcuts in + * [com.android.quickstep.TaskOverlayFactory]. + */ + fun createFactory( + abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper() + ): TaskShortcutFactory = + object : TaskShortcutFactory { + override fun getShortcuts( + container: RecentsViewContainer, + taskContainer: TaskContainer, + ): List? { + return if ( + DesktopModeStatus.canEnterDesktopMode(container.asContext()) && + Flags.moveToExternalDisplayShortcut() + ) + listOf( + ExternalDisplaySystemShortcut( + container, + abstractFloatingViewHelper, + taskContainer, + ) + ) + else null + } + + override fun showForGroupedTask() = true + } + } +} diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 7a0d701724..967bd184c3 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -1493,6 +1493,17 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable { } } + /** Call shell to move a task with given `taskId` to external display. */ + public void moveToExternalDisplay(int taskId) { + if (mDesktopMode != null) { + try { + mDesktopMode.moveToExternalDisplay(taskId); + } catch (RemoteException e) { + Log.w(TAG, "Failed call moveToExternalDisplay", e); + } + } + } + // // Unfold transition // diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java index 8e45767f85..0dbdcb713a 100644 --- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java +++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java @@ -116,6 +116,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride { TaskShortcutFactory.INSTALL, TaskShortcutFactory.FREE_FORM, DesktopSystemShortcut.Companion.createFactory(), + ExternalDisplaySystemShortcut.Companion.createFactory(), TaskShortcutFactory.WELLBEING, TaskShortcutFactory.SAVE_APP_PAIR, TaskShortcutFactory.SCREENSHOT, diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 7554c4496a..bccfa0c756 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -6673,6 +6673,26 @@ public abstract class RecentsView< successCallback.run(); } + /** + * Move the provided task into external display and invoke {@code successCallback} if succeeded. + */ + public void moveTaskToExternalDisplay(TaskContainer taskContainer, Runnable successCallback) { + if (!DesktopModeStatus.canEnterDesktopMode(mContext)) { + return; + } + switchToScreenshot(() -> finishRecentsAnimation(/* toRecents= */true, /* shouldPip= */false, + () -> moveTaskToDesktopInternal(taskContainer, successCallback))); + } + + private void moveTaskToDesktopInternal(TaskContainer taskContainer, Runnable successCallback) { + if (mDesktopRecentsTransitionController == null) { + return; + } + mDesktopRecentsTransitionController.moveToExternalDisplay(taskContainer.getTask().key.id); + successCallback.run(); + } + + // Logs when the orientation of Overview changes. We log both real and fake orientation changes. private void logOrientationChanged() { // Only log when Overview is showing. diff --git a/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt new file mode 100644 index 0000000000..8968b9c55a --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/ExternalDisplaySystemShortcutTest.kt @@ -0,0 +1,193 @@ +/* + * 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.quickstep + +import android.content.ComponentName +import android.content.Intent +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingViewHelper +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.uioverrides.QuickstepLauncher +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.quickstep.TaskOverlayFactory.TaskOverlay +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.TaskContainer +import com.android.quickstep.views.TaskThumbnailViewDeprecated +import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.Task.TaskKey +import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness + +/** Test for ExternalDisplaySystemShortcut */ +class ExternalDisplaySystemShortcutTest { + + @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT) + + private val launcher: QuickstepLauncher = mock() + private val statsLogManager: StatsLogManager = mock() + private val statsLogger: StatsLogManager.StatsLogger = mock() + private val recentsView: LauncherRecentsView = mock() + private val taskView: TaskView = mock() + private val workspaceItemInfo: WorkspaceItemInfo = mock() + private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val iconView: TaskViewIcon = mock() + private val transformingTouchDelegate: TransformingTouchDelegate = mock() + private val factory: TaskShortcutFactory = + ExternalDisplaySystemShortcut.createFactory(abstractFloatingViewHelper) + private val overlayFactory: TaskOverlayFactory = mock() + private val overlay: TaskOverlay<*> = mock() + + private lateinit var mockitoSession: StaticMockitoSession + + @Before + fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.enforceDeviceRestrictions() } + ExtendedMockito.doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + whenever(overlayFactory.createOverlay(any())).thenReturn(overlay) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @EnableFlags(Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT) + fun createExternalDisplayTaskShortcut_desktopModeDisabled() { + val task = createTask() + val taskContainer = createTaskContainer(task) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported() { + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + val taskContainer = createTaskContainer(createTask()) + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun createExternalDisplayTaskShortcut_desktopModeEnabled_deviceNotSupported_overrideEnabled() { + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + ExtendedMockito.doReturn(false).`when` { DesktopModeStatus.enforceDeviceRestrictions() } + + val taskContainer = spy(createTaskContainer(createTask())) + doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).isNotNull() + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_MOVE_TO_EXTERNAL_DISPLAY_SHORTCUT, + ) + fun externalDisplaySystemShortcutClicked() { + val task = createTask() + val taskContainer = spy(createTaskContainer(task)) + + whenever(launcher.getOverviewPanel()).thenReturn(recentsView) + whenever(launcher.statsLogManager).thenReturn(statsLogManager) + whenever(statsLogManager.logger()).thenReturn(statsLogger) + whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger) + whenever(recentsView.moveTaskToExternalDisplay(any(), any())).thenAnswer { + val successCallback = it.getArgument(1) + successCallback.run() + } + doReturn(workspaceItemInfo).whenever(taskContainer).itemInfo + + val shortcuts = factory.getShortcuts(launcher, taskContainer) + assertThat(shortcuts).hasSize(1) + assertThat(shortcuts!!.first()).isInstanceOf(ExternalDisplaySystemShortcut::class.java) + + val externalDisplayShortcut = shortcuts.first() as ExternalDisplaySystemShortcut + + externalDisplayShortcut.onClick(taskView) + + val allTypesExceptRebindSafe = + AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv() + verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe) + verify(recentsView).moveTaskToExternalDisplay(eq(taskContainer), any()) + verify(statsLogger).withItemInfo(workspaceItemInfo) + verify(statsLogger).log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP) + } + + private fun createTask(): Task = Task(TaskKey(1, 0, Intent(), ComponentName("", ""), 0, 2000)) + + private fun createTaskContainer(task: Task): TaskContainer { + val snapshotView = + if (enableRefactorTaskThumbnail()) mock() + else mock() + return TaskContainer( + taskView, + task, + snapshotView, + iconView, + transformingTouchDelegate, + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + digitalWellBeingToast = null, + showWindowsView = null, + overlayFactory, + ) + } +} diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index fbd24d89f0..a48405ca50 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -221,6 +221,9 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped on desktop icon on a task menu.") LAUNCHER_SYSTEM_SHORTCUT_DESKTOP_TAP(1706), + @UiEvent(doc = "Use tapped on external display icon on a task menu,") + LAUNCHER_SYSTEM_SHORTCUT_EXTERNAL_DISPLAY_TAP(1957), + @UiEvent(doc = "User tapped on pause app system shortcut.") LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP(521),