Merge "Add task menu item to move task to external display" into main

This commit is contained in:
Graciela Putri
2024-10-15 16:51:20 +00:00
committed by Android (Google) Code Review
9 changed files with 345 additions and 0 deletions
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M320,840v-80L160,760q-33,0 -56.5,-23.5T80,680v-480q0,-33 23.5,-56.5T160,120h640q33,0 56.5,23.5T880,200v480q0,33 -23.5,56.5T800,760L640,760v80L320,840ZM160,680h640v-480L160,200v480ZM160,680v-480,480Z"
android:fillColor="#e8eaed"/>
</vector>
+2
View File
@@ -28,6 +28,8 @@
<string name="recent_task_option_freeform">Freeform</string>
<!-- Title and content description for an option to enter desktop windowing mode for a given app -->
<string name="recent_task_option_desktop">Desktop</string>
<!-- Title and content description for an option to move app to external display. -->
<string name="recent_task_option_external_display">Move to external display</string>
<!-- Title and content description for Desktop tile in Recents screen that contains apps opened inside desktop windowing mode [CHAR LIMIT=NONE] -->
<string name="recent_task_desktop">Desktop</string>
@@ -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,
@@ -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<RecentsViewContainer>(
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<*, *>>()
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<ExternalDisplaySystemShortcut>? {
return if (
DesktopModeStatus.canEnterDesktopMode(container.asContext()) &&
Flags.moveToExternalDisplayShortcut()
)
listOf(
ExternalDisplaySystemShortcut(
container,
abstractFloatingViewHelper,
taskContainer,
)
)
else null
}
override fun showForGroupedTask() = true
}
}
}
@@ -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
//
@@ -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,
@@ -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.
@@ -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<LauncherRecentsView>()).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<Runnable>(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<TaskThumbnailView>()
else mock<TaskThumbnailViewDeprecated>()
return TaskContainer(
taskView,
task,
snapshotView,
iconView,
transformingTouchDelegate,
SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
digitalWellBeingToast = null,
showWindowsView = null,
overlayFactory,
)
}
}
@@ -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),