Merge "Add task menu item to move task to external display" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
19c00bb849
@@ -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>
|
||||
@@ -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),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user