summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig10
-rw-r--r--libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml1
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt161
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml3
-rw-r--r--libs/WindowManager/Shell/shared/res/values/dimen.xml3
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java65
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt124
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt151
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java64
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt81
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt121
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt95
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt79
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt51
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt285
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md118
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt37
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt168
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt170
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt233
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt74
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt280
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java87
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt47
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java84
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt9
-rw-r--r--libs/androidfw/AssetManager2.cpp82
-rw-r--r--libs/androidfw/Idmap.cpp42
-rw-r--r--libs/androidfw/include/androidfw/AssetManager2.h10
-rw-r--r--libs/androidfw/include/androidfw/Idmap.h43
-rw-r--r--libs/hostgraphics/include/gui/BufferItemConsumer.h29
-rw-r--r--libs/hostgraphics/include/gui/Surface.h4
-rw-r--r--libs/hostgraphics/include/ui/Fence.h4
-rw-r--r--libs/hwui/Android.bp12
-rw-r--r--libs/hwui/framework-graphics-ravenwood-policies.txt1
-rw-r--r--libs/hwui/jni/Bitmap.cpp12
-rw-r--r--libs/hwui/jni/Region.cpp6
-rw-r--r--libs/hwui/jni/ScopedParcel.cpp4
-rw-r--r--libs/hwui/jni/ScopedParcel.h4
-rw-r--r--libs/hwui/jni/graphics_jni_helpers.h1
63 files changed, 2646 insertions, 584 deletions
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index a08f88a5b937..1e72d64397d7 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -184,3 +184,13 @@ flag {
description: "Try out bubble bar on phones"
bug: "394869612"
}
+
+flag {
+ name: "enable_bubble_task_view_listener"
+ namespace: "multitasking"
+ description: "Use the same taskview listener for bubble bar and floating"
+ bug: "272102927"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
index 95cd1c72a2af..800ea7446b6e 100644
--- a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
@@ -2,6 +2,7 @@
package="com.android.wm.shell.multivalenttests">
<uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+ <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT"/>
<application android:debuggable="true" android:supportsRtl="true" >
<uses-library android:name="android.test.runner" />
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 9ebc3d78b3a7..3aefcd5ec6c0 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -16,6 +16,7 @@
package com.android.wm.shell.bubbles
+import android.app.ActivityOptions
import android.app.Notification
import android.app.PendingIntent
import android.content.ComponentName
@@ -24,6 +25,8 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.graphics.drawable.Icon
import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.service.notification.NotificationListenerService.Ranking
import android.service.notification.StatusBarNotification
import android.view.View
@@ -33,6 +36,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_ANYTHING
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener
import com.android.wm.shell.common.TestShellExecutor
@@ -41,6 +45,7 @@ import com.android.wm.shell.taskview.TaskViewController
import com.android.wm.shell.taskview.TaskViewTaskController
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
@@ -48,6 +53,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
@@ -61,6 +67,9 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class BubbleTaskViewListenerTest {
+ @get:Rule
+ val setFlagsRule = SetFlagsRule()
+
private val context = ApplicationProvider.getApplicationContext<Context>()
private var taskViewController = mock<TaskViewController>()
@@ -155,9 +164,22 @@ class BubbleTaskViewListenerTest {
}
getInstrumentation().waitForIdleSync()
- // ..so it's pending intent-based, and launches that
+ // ..so it's pending intent-based, so the pending intent should be active
assertThat(b.isPendingIntentActive).isTrue()
- verify(taskViewController).startActivity(any(), eq(pendingIntent), any(), any(), any())
+
+ val intentCaptor = argumentCaptor<Intent>()
+ val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+ verify(taskViewController).startActivity(any(),
+ eq(pendingIntent),
+ intentCaptor.capture(),
+ optionsCaptor.capture(),
+ any())
+ val intentFlags = intentCaptor.lastValue.flags
+ assertThat((intentFlags and Intent.FLAG_ACTIVITY_NEW_DOCUMENT) != 0).isTrue()
+ assertThat((intentFlags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
+ assertThat(optionsCaptor.lastValue.launchedFromBubble).isTrue()
+ assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
}
@Test
@@ -178,12 +200,52 @@ class BubbleTaskViewListenerTest {
}
getInstrumentation().waitForIdleSync()
- assertThat(b.isPendingIntentActive).isFalse()
- verify(taskViewController).startShortcutActivity(any(), eq(shortcutInfo), any(), any())
+ val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+ assertThat(b.isPendingIntentActive).isFalse() // not triggered for shortcut chats
+ verify(taskViewController).startShortcutActivity(any(),
+ eq(shortcutInfo),
+ optionsCaptor.capture(),
+ any())
+ assertThat(optionsCaptor.lastValue.launchedFromBubble).isTrue()
+ assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isTrue()
+ assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
}
+ @EnableFlags(FLAG_ENABLE_BUBBLE_ANYTHING)
@Test
- fun onInitialized_appBubble() {
+ fun onInitialized_shortcutBubble() {
+ val shortcutInfo = ShortcutInfo.Builder(context)
+ .setId("mockShortcutId")
+ .build()
+
+ val b = createShortcutBubble(shortcutInfo)
+ bubbleTaskViewListener.setBubble(b)
+
+ assertThat(b.isChat).isFalse()
+ assertThat(b.isShortcut).isTrue()
+ assertThat(b.shortcutInfo).isNotNull()
+
+ getInstrumentation().runOnMainSync {
+ bubbleTaskViewListener.onInitialized()
+ }
+ getInstrumentation().waitForIdleSync()
+
+ val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+ assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
+ verify(taskViewController).startShortcutActivity(any(),
+ eq(shortcutInfo),
+ optionsCaptor.capture(),
+ any())
+ assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.isApplyMultipleTaskFlagForShortcut).isTrue()
+ assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
+ }
+
+ @Test
+ fun onInitialized_appBubble_intent() {
val b = createAppBubble()
bubbleTaskViewListener.setBubble(b)
@@ -194,11 +256,83 @@ class BubbleTaskViewListenerTest {
}
getInstrumentation().waitForIdleSync()
- assertThat(b.isPendingIntentActive).isFalse()
- verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any())
+ val intentCaptor = argumentCaptor<Intent>()
+ val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+ assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
+ verify(taskViewController).startActivity(any(),
+ any(),
+ intentCaptor.capture(),
+ optionsCaptor.capture(),
+ any())
+
+ assertThat((intentCaptor.lastValue.flags
+ and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
+ assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
}
@Test
+ fun onInitialized_appBubble_pendingIntent() {
+ val b = createAppBubble(usePendingIntent = true)
+ bubbleTaskViewListener.setBubble(b)
+
+ assertThat(b.isApp).isTrue()
+
+ getInstrumentation().runOnMainSync {
+ bubbleTaskViewListener.onInitialized()
+ }
+ getInstrumentation().waitForIdleSync()
+
+ val intentCaptor = argumentCaptor<Intent>()
+ val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+ assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
+ verify(taskViewController).startActivity(any(),
+ any(),
+ intentCaptor.capture(),
+ optionsCaptor.capture(),
+ any())
+
+ assertThat((intentCaptor.lastValue.flags
+ and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
+ assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
+ }
+
+ @Test
+ fun onInitialized_noteBubble() {
+ val b = createNoteBubble()
+ bubbleTaskViewListener.setBubble(b)
+
+ assertThat(b.isNote).isTrue()
+
+ getInstrumentation().runOnMainSync {
+ bubbleTaskViewListener.onInitialized()
+ }
+ getInstrumentation().waitForIdleSync()
+
+ val intentCaptor = argumentCaptor<Intent>()
+ val optionsCaptor = argumentCaptor<ActivityOptions>()
+
+ assertThat(b.isPendingIntentActive).isFalse() // chat only triggers setting it active
+ verify(taskViewController).startActivity(any(),
+ any(),
+ intentCaptor.capture(),
+ optionsCaptor.capture(),
+ any())
+
+ assertThat((intentCaptor.lastValue.flags
+ and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
+ assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
+ assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
+ }
+
+
+ @Test
fun onInitialized_preparingTransition() {
val b = createAppBubble()
bubbleTaskViewListener.setBubble(b)
@@ -416,13 +550,24 @@ class BubbleTaskViewListenerTest {
assertThat(isNew).isTrue()
}
- private fun createAppBubble(): Bubble {
+ private fun createAppBubble(usePendingIntent: Boolean = false): Bubble {
val target = Intent(context, TestActivity::class.java)
target.setPackage(context.packageName)
+ if (usePendingIntent) {
+ // Robolectric doesn't seem to play nice with PendingIntents, have to mock it.
+ val pendingIntent = mock<PendingIntent>()
+ whenever(pendingIntent.intent).thenReturn(target)
+ return Bubble.createAppBubble(pendingIntent, mock<UserHandle>(),
+ mainExecutor, bgExecutor)
+ }
return Bubble.createAppBubble(target, mock<UserHandle>(), mock<Icon>(),
mainExecutor, bgExecutor)
}
+ private fun createShortcutBubble(shortcutInfo: ShortcutInfo): Bubble {
+ return Bubble.createShortcutBubble(shortcutInfo, mainExecutor, bgExecutor)
+ }
+
private fun createNoteBubble(): Bubble {
val target = Intent(context, TestActivity::class.java)
target.setPackage(context.packageName)
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f5f3f0fe52eb..a0c68ad44379 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -670,7 +670,4 @@
<dimen name="desktop_windowing_education_promo_height">352dp</dimen>
<!-- The corner radius of the desktop windowing education promo. -->
<dimen name="desktop_windowing_education_promo_corner_radius">28dp</dimen>
-
- <!-- The corner radius of freeform tasks in desktop windowing. -->
- <dimen name="desktop_windowing_freeform_rounded_corner_radius">16dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml
index 11a6f32d7454..23c9caf2046c 100644
--- a/libs/WindowManager/Shell/shared/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml
@@ -46,4 +46,7 @@
<dimen name="drop_target_expanded_view_height">578</dimen>
<dimen name="drop_target_expanded_view_padding_bottom">108</dimen>
<dimen name="drop_target_expanded_view_padding_horizontal">24</dimen>
+
+ <!-- The corner radius of freeform tasks in desktop windowing. -->
+ <dimen name="desktop_windowing_freeform_rounded_corner_radius">16dp</dimen>
</resources> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
index 14338a49ee2f..0e4a6b9fb083 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt
@@ -16,12 +16,14 @@
package com.android.wm.shell.shared.desktopmode
+import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.app.TaskInfo
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED
import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION
import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS
+import android.content.pm.PackageManager
import android.window.DesktopModeFlags
import com.android.internal.R
@@ -32,8 +34,10 @@ import com.android.internal.R
class DesktopModeCompatPolicy(private val context: Context) {
private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi)
+ private val pkgManager: PackageManager
+ get() = context.getPackageManager()
private val defaultHomePackage: String?
- get() = context.getPackageManager().getHomeActivities(ArrayList())?.packageName
+ get() = pkgManager.getHomeActivities(ArrayList())?.packageName
/**
* If the top activity should be exempt from desktop windowing and forced back to fullscreen.
@@ -47,11 +51,12 @@ class DesktopModeCompatPolicy(private val context: Context) {
fun isTopActivityExemptFromDesktopWindowing(packageName: String?,
numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) =
- DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue
- && ((isSystemUiTask(packageName)
- || isPartOfDefaultHomePackageOrNoHomeAvailable(packageName)
- || isTransparentTask(isActivityStackTransparent, numActivities))
- && !isTopActivityNoDisplay)
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue &&
+ ((isSystemUiTask(packageName) ||
+ isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) ||
+ (isTransparentTask(isActivityStackTransparent, numActivities) &&
+ hasFullscreenTransparentPermission(packageName))) &&
+ !isTopActivityNoDisplay)
/**
* Whether the caption insets should be excluded from configuration for system to handle.
@@ -83,6 +88,26 @@ class DesktopModeCompatPolicy(private val context: Context) {
private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage
+ // Checks if the app for the given package has the SYSTEM_ALERT_WINDOW permission.
+ private fun hasFullscreenTransparentPermission(packageName: String?): Boolean {
+ if (DesktopModeFlags.ENABLE_MODALS_FULLSCREEN_WITH_PERMISSIONS.isTrue) {
+ if (packageName == null) {
+ return false
+ }
+ return try {
+ val packageInfo = pkgManager.getPackageInfo(
+ packageName,
+ PackageManager.GET_PERMISSIONS
+ )
+ packageInfo?.requestedPermissions?.contains(SYSTEM_ALERT_WINDOW) == true
+ } catch (e: PackageManager.NameNotFoundException) {
+ false // Package not found
+ }
+ }
+ // If the flag is disabled we make this condition neutral.
+ return true
+ }
+
/**
* Returns true if the tasks base activity is part of the default home package, or there is
* currently no default home package available.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index 5bd8d86f1144..0f1bf5e09751 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -15,6 +15,8 @@
*/
package com.android.wm.shell.bubbles;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
+
import android.annotation.DrawableRes;
import android.annotation.Nullable;
import android.content.Context;
@@ -35,7 +37,6 @@ import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.icons.DotRenderer;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
import com.android.wm.shell.shared.animation.Interpolators;
@@ -132,7 +133,7 @@ public class BadgedImageView extends ConstraintLayout {
private void getOutline(Outline outline) {
final int bubbleSize = mPositioner.getBubbleSize();
- final int normalizedSize = IconNormalizer.getNormalizedCircleSize(bubbleSize);
+ final int normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * bubbleSize);
final int inset = (bubbleSize - normalizedSize) / 2;
outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index d9489287ff42..313d151aeab7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -364,7 +364,7 @@ public class Bubble implements BubbleViewProvider {
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
return new Bubble(intent,
user,
- /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
+ /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user),
mainExecutor, bgExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index f6a2c8d9695e..305fcdd5fb7d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -912,7 +912,7 @@ public class BubbleController implements ConfigurationChangeListener,
// TODO(b/393172431) : Utilise DragZoneFactory once it is ready
final int bubbleBarDropZoneSideSize = getContext().getResources().getDimensionPixelSize(
R.dimen.bubble_bar_drop_zone_side_size);
- int top = t - bubbleBarDropZoneSideSize;
+ int top = b - bubbleBarDropZoneSideSize;
result.put(BubbleBarLocation.LEFT,
new Rect(l, top, l + bubbleBarDropZoneSideSize, b));
result.put(BubbleBarLocation.RIGHT,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 3f607a9c52ef..2c2451cab999 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -197,6 +197,8 @@ public class BubbleExpandedView extends LinearLayout {
*/
private final FrameLayout mExpandedViewContainer = new FrameLayout(getContext());
+ private TaskView.Listener mCurrentTaskViewListener;
+
private final TaskView.Listener mTaskViewListener = new TaskView.Listener() {
private boolean mInitialized = false;
private boolean mDestroyed = false;
@@ -235,18 +237,24 @@ public class BubbleExpandedView extends LinearLayout {
Context context =
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
+ Intent fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
PendingIntent pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
- mBubble.getIntent().addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
- PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
+ mBubble.getIntent(),
+ // Needs to be mutable for the fillInIntent
+ PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
/* options= */ null);
- mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
- launchBounds);
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
} else if (!mIsOverflow && isShortcutBubble) {
ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey());
- options.setLaunchedFromBubble(true);
- options.setApplyActivityFlagsForBubbles(true);
+ if (mBubble.isChat()) {
+ options.setLaunchedFromBubble(true);
+ options.setApplyActivityFlagsForBubbles(true);
+ } else {
+ options.setApplyMultipleTaskFlagForShortcut(true);
+ }
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
options, launchBounds);
} else {
@@ -453,7 +461,34 @@ public class BubbleExpandedView extends LinearLayout {
mTaskView = bubbleTaskView.getTaskView();
// reset the insets that might left after TaskView is shown in BubbleBarExpandedView
mTaskView.setCaptionInsets(null);
- bubbleTaskView.setDelegateListener(mTaskViewListener);
+ if (Flags.enableBubbleTaskViewListener()) {
+ mCurrentTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView,
+ /* viewParent= */ this, expandedViewManager,
+ new BubbleTaskViewListener.Callback() {
+ @Override
+ public void onTaskCreated() {
+ setContentVisibility(true);
+ }
+
+ @Override
+ public void onContentVisibilityChanged(boolean visible) {
+ setContentVisibility(visible);
+ }
+
+ @Override
+ public void onBackPressed() {
+ mStackView.onBackPressed();
+ }
+
+ @Override
+ public void onTaskRemovalStarted() {
+ // nothing to do / handled in listener.
+ }
+ });
+ } else {
+ mCurrentTaskViewListener = mTaskViewListener;
+ bubbleTaskView.setDelegateListener(mCurrentTaskViewListener);
+ }
// set a fixed width so it is not recalculated as part of a rotation. the width will be
// updated manually after the rotation.
@@ -464,9 +499,12 @@ public class BubbleExpandedView extends LinearLayout {
}
mExpandedViewContainer.addView(mTaskView, lp);
bringChildToFront(mTaskView);
- if (bubbleTaskView.isCreated()) {
- mTaskViewListener.onTaskCreated(
- bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName());
+
+ if (!Flags.enableBubbleTaskViewListener()) {
+ if (bubbleTaskView.isCreated()) {
+ mCurrentTaskViewListener.onTaskCreated(
+ bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName());
+ }
}
}
}
@@ -897,7 +935,12 @@ public class BubbleExpandedView extends LinearLayout {
Log.w(TAG, "Stack is null for bubble: " + bubble);
return;
}
- boolean isNew = mBubble == null || didBackingContentChange(bubble);
+ boolean isNew;
+ if (mCurrentTaskViewListener instanceof BubbleTaskViewListener) {
+ isNew = ((BubbleTaskViewListener) mCurrentTaskViewListener).setBubble(bubble);
+ } else {
+ isNew = mBubble == null || didBackingContentChange(bubble);
+ }
boolean isUpdate = bubble != null && mBubble != null
&& bubble.getKey().equals(mBubble.getKey());
ProtoLog.d(WM_SHELL_BUBBLES, "BubbleExpandedView - update bubble=%s; isNew=%b; isUpdate=%b",
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 221c9332711e..33f1b94bac73 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.bubbles;
+import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES;
import android.content.Context;
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
-import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -557,8 +557,7 @@ public class BubblePositioner implements BubbleDropTargetBoundsProvider {
public float getPointerPosition(float bubblePosition) {
// TODO: I don't understand why it works but it does - why normalized in portrait
// & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation?
- final float normalizedSize = IconNormalizer.getNormalizedCircleSize(
- getBubbleSize());
+ final float normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * getBubbleSize());
return showBubblesVertically()
? bubblePosition + (getBubbleSize() / 2f)
: bubblePosition + (normalizedSize / 2f) - mPointerWidth;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
index a38debb702dc..63d713495177 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
@@ -129,27 +129,28 @@ public class BubbleTaskViewListener implements TaskView.Listener {
Context context =
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
- Intent fillInIntent = null;
+ Intent fillInIntent = new Intent();
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
// First try get pending intent from the bubble
PendingIntent pi = mBubble.getPendingIntent();
if (pi == null) {
- // If null - create new one
+ // If null - create new one based on the bubble intent
pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
- mBubble.getIntent()
- .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
- PendingIntent.FLAG_IMMUTABLE
- | PendingIntent.FLAG_UPDATE_CURRENT,
+ mBubble.getIntent(),
+ // Needs to be mutable for the fillInIntent
+ PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
/* options= */ null);
- } else {
- fillInIntent = new Intent(pi.getIntent());
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
}
mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
} else if (isShortcutBubble) {
- options.setLaunchedFromBubble(true);
- options.setApplyActivityFlagsForBubbles(true);
+ if (mBubble.isChat()) {
+ options.setLaunchedFromBubble(true);
+ options.setApplyActivityFlagsForBubbles(true);
+ } else {
+ options.setApplyMultipleTaskFlagForShortcut(true);
+ }
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
options, launchBounds);
} else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index a676f41baafe..338ffe76e6ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.View.INVISIBLE;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE;
@@ -325,7 +326,7 @@ public class BubbleTransitions {
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change chg = info.getChanges().get(i);
if (chg.getTaskInfo() == null) continue;
- if (chg.getMode() != TRANSIT_CHANGE) continue;
+ if (chg.getMode() != TRANSIT_CHANGE && chg.getMode() != TRANSIT_TO_FRONT) continue;
if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue;
mStartBounds.set(chg.getStartAbsBounds());
// Converting a task into taskview, so treat as "new"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index e3b0872df593..29837dc04423 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -135,6 +135,7 @@ public class BubbleBarLayerView extends FrameLayout
/** Shows the expanded view drop target at the requested {@link BubbleBarLocation location} */
public void showBubbleBarExtendedViewDropTarget(@NonNull BubbleBarLocation bubbleBarLocation) {
+ setVisibility(VISIBLE);
mBubbleExpandedViewPinController.showDropTarget(bubbleBarLocation);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 8377a35a9e7d..87a4115ccd3a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -324,8 +324,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
applyVisibilityToLeash(imeSourceControl);
}
- if (!mImeShowing) {
- removeImeSurface(mDisplayId);
+ if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (!mImeShowing) {
+ removeImeSurface(mDisplayId);
+ }
}
}
} else {
@@ -663,7 +665,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
ImeTracker.forLogging().onProgress(mStatsToken,
ImeTracker.PHASE_WM_ANIMATION_RUNNING);
t.hide(animatingLeash);
- removeImeSurface(mDisplayId);
+ if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+ removeImeSurface(mDisplayId);
+ }
if (android.view.inputmethod.Flags.refactorInsetsController()) {
setVisibleDirectly(false /* visible */, statsToken);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt
new file mode 100644
index 000000000000..7a5bc1383ccf
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorController.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.wm.shell.common
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.graphics.RectF
+import android.view.SurfaceControl
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.shared.annotations.ShellDesktopThread
+
+/**
+ * Controller to manage the indicators that show users the current position of the dragged window on
+ * the new display when performing drag move across displays.
+ */
+class MultiDisplayDragMoveIndicatorController(
+ private val displayController: DisplayController,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val indicatorSurfaceFactory: MultiDisplayDragMoveIndicatorSurface.Factory,
+ @ShellDesktopThread private val desktopExecutor: ShellExecutor,
+) {
+ @ShellDesktopThread
+ private val dragIndicators =
+ mutableMapOf<Int, MutableMap<Int, MultiDisplayDragMoveIndicatorSurface>>()
+
+ /**
+ * Called during drag move, which started at [startDisplayId]. Updates the position and
+ * visibility of the drag move indicators for the [taskInfo] based on [boundsDp] on the
+ * destination displays ([displayIds]) as the dragged window moves. [transactionSupplier]
+ * provides a [SurfaceControl.Transaction] for applying changes to the indicator surfaces.
+ *
+ * It is executed on the [desktopExecutor] to prevent blocking the main thread and avoid jank,
+ * as creating and manipulating surfaces can be expensive.
+ */
+ fun onDragMove(
+ boundsDp: RectF,
+ startDisplayId: Int,
+ taskInfo: RunningTaskInfo,
+ displayIds: Set<Int>,
+ transactionSupplier: () -> SurfaceControl.Transaction,
+ ) {
+ desktopExecutor.execute {
+ for (displayId in displayIds) {
+ if (displayId == startDisplayId) {
+ // No need to render indicators on the original display where the drag started.
+ continue
+ }
+ val displayLayout = displayController.getDisplayLayout(displayId) ?: continue
+ val shouldBeVisible =
+ RectF.intersects(RectF(boundsDp), displayLayout.globalBoundsDp())
+ if (
+ dragIndicators[taskInfo.taskId]?.containsKey(displayId) != true &&
+ !shouldBeVisible
+ ) {
+ // Skip this display if:
+ // - It doesn't have an existing indicator that needs to be updated, AND
+ // - The latest dragged window bounds don't intersect with this display.
+ continue
+ }
+
+ val boundsPx =
+ MultiDisplayDragMoveBoundsCalculator.convertGlobalDpToLocalPxForRect(
+ boundsDp,
+ displayLayout,
+ )
+
+ // Get or create the inner map for the current task.
+ val dragIndicatorsForTask =
+ dragIndicators.getOrPut(taskInfo.taskId) { mutableMapOf() }
+ dragIndicatorsForTask[displayId]?.also { existingIndicator ->
+ val transaction = transactionSupplier()
+ existingIndicator.relayout(boundsPx, transaction, shouldBeVisible)
+ transaction.apply()
+ } ?: run {
+ val newIndicator =
+ indicatorSurfaceFactory.create(
+ taskInfo,
+ displayController.getDisplay(displayId),
+ )
+ newIndicator.show(
+ transactionSupplier(),
+ taskInfo,
+ rootTaskDisplayAreaOrganizer,
+ displayId,
+ boundsPx,
+ )
+ dragIndicatorsForTask[displayId] = newIndicator
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when the drag ends. Disposes of the drag move indicator surfaces associated with the
+ * given [taskId]. [transactionSupplier] provides a [SurfaceControl.Transaction] for applying
+ * changes to the indicator surfaces.
+ *
+ * It is executed on the [desktopExecutor] to ensure that any pending `onDragMove` operations
+ * have completed before disposing of the surfaces.
+ */
+ fun onDragEnd(taskId: Int, transactionSupplier: () -> SurfaceControl.Transaction) {
+ desktopExecutor.execute {
+ dragIndicators.remove(taskId)?.values?.takeIf { it.isNotEmpty() }?.let { indicators ->
+ val transaction = transactionSupplier()
+ indicators.forEach { indicator ->
+ indicator.disposeSurface(transaction)
+ }
+ transaction.apply()
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt
new file mode 100644
index 000000000000..d05d3b0903d7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorSurface.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.wm.shell.common
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Rect
+import android.os.Trace
+import android.view.Display
+import android.view.SurfaceControl
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.ui.graphics.toArgb
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.Theme
+
+/**
+ * Represents the indicator surface that visualizes the current position of a dragged window during
+ * a multi-display drag operation.
+ *
+ * This class manages the creation, display, and manipulation of the [SurfaceControl]s that act as a
+ * visual indicator, providing feedback to the user about the dragged window's location.
+ */
+class MultiDisplayDragMoveIndicatorSurface(
+ context: Context,
+ taskInfo: RunningTaskInfo,
+ display: Display,
+ surfaceControlBuilderFactory: Factory.SurfaceControlBuilderFactory,
+) {
+ private var isVisible = false
+
+ // A container surface to host the veil background
+ private var veilSurface: SurfaceControl? = null
+
+ private val decorThemeUtil = DecorThemeUtil(context)
+ private val lightColors = dynamicLightColorScheme(context)
+ private val darkColors = dynamicDarkColorScheme(context)
+
+ init {
+ Trace.beginSection("DragIndicatorSurface#init")
+
+ val displayId = display.displayId
+ veilSurface =
+ surfaceControlBuilderFactory
+ .create("Drag indicator veil of Task=${taskInfo.taskId} Display=$displayId")
+ .setColorLayer()
+ .setCallsite("DragIndicatorSurface#init")
+ .setHidden(true)
+ .build()
+
+ // TODO: b/383069173 - Add icon for the surface.
+
+ Trace.endSection()
+ }
+
+ /**
+ * Disposes the indicator surface using the provided [transaction].
+ */
+ fun disposeSurface(transaction: SurfaceControl.Transaction) {
+ veilSurface?.let { veil -> transaction.remove(veil) }
+ veilSurface = null
+ }
+
+ /**
+ * Shows the indicator surface at [bounds] on the specified display ([displayId]),
+ * visualizing the drag of the [taskInfo]. The indicator surface is shown using [transaction],
+ * and the [rootTaskDisplayAreaOrganizer] is used to reparent the surfaces.
+ */
+ fun show(
+ transaction: SurfaceControl.Transaction,
+ taskInfo: RunningTaskInfo,
+ rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ displayId: Int,
+ bounds: Rect,
+ ) {
+ val backgroundColor =
+ when (decorThemeUtil.getAppTheme(taskInfo)) {
+ Theme.LIGHT -> lightColors.surfaceContainer
+ Theme.DARK -> darkColors.surfaceContainer
+ }
+ val veil = veilSurface ?: return
+ isVisible = true
+
+ rootTaskDisplayAreaOrganizer.reparentToDisplayArea(displayId, veil, transaction)
+ relayout(bounds, transaction, shouldBeVisible = true)
+ transaction.show(veil).setColor(veil, Color.valueOf(backgroundColor.toArgb()).components)
+ transaction.apply()
+ }
+
+ /**
+ * Repositions and resizes the indicator surface based on [bounds] using [transaction]. The
+ * [shouldBeVisible] flag indicates whether the indicator is within the display after relayout.
+ */
+ fun relayout(bounds: Rect, transaction: SurfaceControl.Transaction, shouldBeVisible: Boolean) {
+ if (!isVisible && !shouldBeVisible) {
+ // No need to relayout if the surface is already invisible and should not be visible.
+ return
+ }
+ isVisible = shouldBeVisible
+ val veil = veilSurface ?: return
+ transaction.setCrop(veil, bounds)
+ }
+
+ /**
+ * Factory for creating [MultiDisplayDragMoveIndicatorSurface] instances with the [context].
+ */
+ class Factory(private val context: Context) {
+ private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory =
+ object : SurfaceControlBuilderFactory {}
+
+ /**
+ * Creates a new [MultiDisplayDragMoveIndicatorSurface] instance to visualize the drag
+ * operation of the [taskInfo] on the given [display].
+ */
+ fun create(
+ taskInfo: RunningTaskInfo,
+ display: Display,
+ ) = MultiDisplayDragMoveIndicatorSurface(
+ context,
+ taskInfo,
+ display,
+ surfaceControlBuilderFactory,
+ )
+
+ /**
+ * Interface for creating [SurfaceControl.Builder] instances.
+ *
+ * This provides an abstraction over [SurfaceControl.Builder] creation for testing purposes.
+ */
+ interface SurfaceControlBuilderFactory {
+ fun create(name: String): SurfaceControl.Builder {
+ return SurfaceControl.Builder().setName(name)
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
index 8e026f04ac31..04e8d8dee520 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
@@ -24,6 +24,7 @@ import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
+import android.util.Rational;
import android.util.Size;
import android.view.Gravity;
@@ -41,9 +42,6 @@ public class PipBoundsAlgorithm {
private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
private static final float INVALID_SNAP_FRACTION = -1f;
- // The same value (with the same name) is used in Launcher.
- private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f;
-
@NonNull private final PipBoundsState mPipBoundsState;
@NonNull protected final PipDisplayLayoutState mPipDisplayLayoutState;
@NonNull protected final SizeSpecSource mSizeSpecSource;
@@ -223,9 +221,8 @@ public class PipBoundsAlgorithm {
+ " than destination(%s)", sourceRectHint, destinationBounds);
return false;
}
- final float reportedRatio = destinationBounds.width() / (float) destinationBounds.height();
- final float inferredRatio = sourceRectHint.width() / (float) sourceRectHint.height();
- if (Math.abs(reportedRatio - inferredRatio) > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
+ if (!PictureInPictureParams.isSameAspectRatio(sourceRectHint,
+ new Rational(destinationBounds.width(), destinationBounds.height()))) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"isSourceRectHintValidForEnterPip=false, hint(%s) does not match"
+ " destination(%s) aspect ratio", sourceRectHint, destinationBounds);
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 02a080017fa6..5d5e4d3ec758 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
@@ -68,6 +68,8 @@ import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.LaunchAdjacentController;
+import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController;
+import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorSurface;
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -82,6 +84,7 @@ import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler;
+import com.android.wm.shell.desktopmode.DesktopDisplayModeController;
import com.android.wm.shell.desktopmode.DesktopImmersiveController;
import com.android.wm.shell.desktopmode.DesktopMinimizationTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
@@ -990,7 +993,8 @@ public abstract class WMShellModule {
WindowDecorTaskResourceLoader taskResourceLoader,
RecentsTransitionHandler recentsTransitionHandler,
DesktopModeCompatPolicy desktopModeCompatPolicy,
- DesktopTilingDecorViewModel desktopTilingDecorViewModel
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel,
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController
) {
if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
return Optional.empty();
@@ -1007,7 +1011,30 @@ public abstract class WMShellModule {
windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy,
- desktopTilingDecorViewModel));
+ desktopTilingDecorViewModel,
+ multiDisplayDragMoveIndicatorController));
+ }
+
+ @WMSingleton
+ @Provides
+ static MultiDisplayDragMoveIndicatorController
+ providesMultiDisplayDragMoveIndicatorController(
+ DisplayController displayController,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ MultiDisplayDragMoveIndicatorSurface.Factory
+ multiDisplayDragMoveIndicatorSurfaceFactory,
+ @ShellDesktopThread ShellExecutor desktopExecutor
+ ) {
+ return new MultiDisplayDragMoveIndicatorController(
+ displayController, rootTaskDisplayAreaOrganizer,
+ multiDisplayDragMoveIndicatorSurfaceFactory, desktopExecutor);
+ }
+
+ @WMSingleton
+ @Provides
+ static MultiDisplayDragMoveIndicatorSurface.Factory
+ providesMultiDisplayDragMoveIndicatorSurfaceFactory(Context context) {
+ return new MultiDisplayDragMoveIndicatorSurface.Factory(context);
}
@WMSingleton
@@ -1233,13 +1260,10 @@ public abstract class WMShellModule {
static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler(
Context context,
ShellInit shellInit,
- Transitions transitions,
DisplayController displayController,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- IWindowManager windowManager,
Optional<DesktopUserRepositories> desktopUserRepositories,
Optional<DesktopTasksController> desktopTasksController,
- ShellTaskOrganizer shellTaskOrganizer
+ Optional<DesktopDisplayModeController> desktopDisplayModeController
) {
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
@@ -1248,13 +1272,10 @@ public abstract class WMShellModule {
new DesktopDisplayEventHandler(
context,
shellInit,
- transitions,
displayController,
- rootTaskDisplayAreaOrganizer,
- windowManager,
desktopUserRepositories.get(),
desktopTasksController.get(),
- shellTaskOrganizer));
+ desktopDisplayModeController.get()));
}
@WMSingleton
@@ -1384,6 +1405,29 @@ public abstract class WMShellModule {
return new DesktopModeUiEventLogger(uiEventLogger, packageManager);
}
+ @WMSingleton
+ @Provides
+ static Optional<DesktopDisplayModeController> provideDesktopDisplayModeController(
+ Context context,
+ Transitions transitions,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ IWindowManager windowManager,
+ ShellTaskOrganizer shellTaskOrganizer,
+ DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider
+ ) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new DesktopDisplayModeController(
+ context,
+ transitions,
+ rootTaskDisplayAreaOrganizer,
+ windowManager,
+ shellTaskOrganizer,
+ desktopWallpaperActivityTokenProvider));
+ }
+
//
// App zoom out
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index c38558d7bde9..946e7952dd50 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -16,41 +16,25 @@
package com.android.wm.shell.desktopmode
-import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
-import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
-import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
-import android.app.WindowConfiguration.windowingModeToString
import android.content.Context
-import android.provider.Settings
-import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
-import android.view.IWindowManager
-import android.view.WindowManager.TRANSIT_CHANGE
import android.window.DesktopExperienceFlags
-import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer
-import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
-import com.android.wm.shell.transition.Transitions
/** Handles display events in desktop mode */
class DesktopDisplayEventHandler(
private val context: Context,
shellInit: ShellInit,
- private val transitions: Transitions,
private val displayController: DisplayController,
- private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
- private val windowManager: IWindowManager,
private val desktopUserRepositories: DesktopUserRepositories,
private val desktopTasksController: DesktopTasksController,
- private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val desktopDisplayModeController: DesktopDisplayModeController,
) : OnDisplaysChangedListener, OnDeskRemovedListener {
private val desktopRepository: DesktopRepository
@@ -70,7 +54,7 @@ class DesktopDisplayEventHandler(
override fun onDisplayAdded(displayId: Int) {
if (displayId != DEFAULT_DISPLAY) {
- refreshDisplayWindowingMode()
+ desktopDisplayModeController.refreshDisplayWindowingMode()
}
if (!supportsDesks(displayId)) {
@@ -88,7 +72,7 @@ class DesktopDisplayEventHandler(
override fun onDisplayRemoved(displayId: Int) {
if (displayId != DEFAULT_DISPLAY) {
- refreshDisplayWindowingMode()
+ desktopDisplayModeController.refreshDisplayWindowingMode()
}
// TODO: b/362720497 - move desks in closing display to the remaining desk.
@@ -102,65 +86,6 @@ class DesktopDisplayEventHandler(
}
}
- private fun refreshDisplayWindowingMode() {
- if (!Flags.enableDisplayWindowingModeSwitching()) return
- // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
- val isExtendedDisplayEnabled =
- 0 !=
- Settings.Global.getInt(
- context.contentResolver,
- DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
- 0,
- )
- if (!isExtendedDisplayEnabled) {
- // No action needed in mirror or projected mode.
- return
- }
-
- val hasNonDefaultDisplay =
- rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId ->
- displayId != DEFAULT_DISPLAY
- }
- val targetDisplayWindowingMode =
- if (hasNonDefaultDisplay) {
- WINDOWING_MODE_FREEFORM
- } else {
- // Use the default display windowing mode when no non-default display.
- windowManager.getWindowingMode(DEFAULT_DISPLAY)
- }
- val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
- requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
- val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
- if (currentDisplayWindowingMode == targetDisplayWindowingMode) {
- // Already in the target mode.
- return
- }
-
- logV(
- "As an external display is connected, changing default display's windowing mode from" +
- " ${windowingModeToString(currentDisplayWindowingMode)}" +
- " to ${windowingModeToString(targetDisplayWindowingMode)}"
- )
-
- val wct = WindowContainerTransaction()
- wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode)
- shellTaskOrganizer
- .getRunningTasks(DEFAULT_DISPLAY)
- .filter { it.activityType == ACTIVITY_TYPE_STANDARD }
- .forEach {
- // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy
- when (it.windowingMode) {
- currentDisplayWindowingMode -> {
- wct.setWindowingMode(it.token, currentDisplayWindowingMode)
- }
- targetDisplayWindowingMode -> {
- wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED)
- }
- }
- }
- transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
- }
-
// TODO: b/362720497 - connected/projected display considerations.
private fun supportsDesks(displayId: Int): Boolean =
DesktopModeStatus.canEnterDesktopMode(context)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
new file mode 100644
index 000000000000..c9a63ff818f5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.app.WindowConfiguration.windowingModeToString
+import android.content.Context
+import android.provider.Settings
+import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.IWindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.WindowContainerTransaction
+import com.android.internal.protolog.ProtoLog
+import com.android.window.flags.Flags
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.transition.Transitions
+
+/** Controls the display windowing mode in desktop mode */
+class DesktopDisplayModeController(
+ private val context: Context,
+ private val transitions: Transitions,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val windowManager: IWindowManager,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
+) {
+
+ fun refreshDisplayWindowingMode() {
+ if (!Flags.enableDisplayWindowingModeSwitching()) return
+ // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
+ val isExtendedDisplayEnabled =
+ 0 !=
+ Settings.Global.getInt(
+ context.contentResolver,
+ DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+ 0,
+ )
+ if (!isExtendedDisplayEnabled) {
+ // No action needed in mirror or projected mode.
+ return
+ }
+
+ val hasNonDefaultDisplay =
+ rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId ->
+ displayId != DEFAULT_DISPLAY
+ }
+ val targetDisplayWindowingMode =
+ if (hasNonDefaultDisplay) {
+ WINDOWING_MODE_FREEFORM
+ } else {
+ // Use the default display windowing mode when no non-default display.
+ windowManager.getWindowingMode(DEFAULT_DISPLAY)
+ }
+ val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
+ requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
+ val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
+ if (currentDisplayWindowingMode == targetDisplayWindowingMode) {
+ // Already in the target mode.
+ return
+ }
+
+ logV(
+ "As an external display is connected, changing default display's windowing mode from" +
+ " ${windowingModeToString(currentDisplayWindowingMode)}" +
+ " to ${windowingModeToString(targetDisplayWindowingMode)}"
+ )
+
+ val wct = WindowContainerTransaction()
+ wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode)
+ shellTaskOrganizer
+ .getRunningTasks(DEFAULT_DISPLAY)
+ .filter { it.activityType == ACTIVITY_TYPE_STANDARD }
+ .forEach {
+ // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy
+ when (it.windowingMode) {
+ currentDisplayWindowingMode -> {
+ wct.setWindowingMode(it.token, currentDisplayWindowingMode)
+ }
+ targetDisplayWindowingMode -> {
+ wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED)
+ }
+ }
+ }
+ // The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display
+ // right after the first launch while its resolved windowing mode is FULLSCREEN. We here
+ // it has the FULLSCREEN override windowing mode.
+ desktopWallpaperActivityTokenProvider.getToken(DEFAULT_DISPLAY)?.let { token ->
+ wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN)
+ }
+ transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
+ }
+
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ companion object {
+ private const val TAG = "DesktopDisplayModeController"
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index eba1be517147..7c6cf4a8b37f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -208,6 +208,7 @@ class DesktopRepository(
/** Adds the given desk under the given display. */
fun addDesk(displayId: Int, deskId: Int) {
+ logD("addDesk for displayId=%d and deskId=%d", displayId, deskId)
desktopData.createDesk(displayId, deskId)
}
@@ -224,6 +225,7 @@ class DesktopRepository(
/** Sets the given desk as the active one in the given display. */
fun setActiveDesk(displayId: Int, deskId: Int) {
+ logD("setActiveDesk for displayId=%d and deskId=%d", displayId, deskId)
desktopData.setActiveDesk(displayId = displayId, deskId = deskId)
}
@@ -246,6 +248,7 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) {
+ logD("addTask for displayId=%d, taskId=%d, isVisible=%b", displayId, taskId, isVisible)
val activeDesk =
checkNotNull(desktopData.getDefaultDesk(displayId)) {
"Expected desk in display: $displayId"
@@ -254,6 +257,13 @@ class DesktopRepository(
}
fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) {
+ logD(
+ "addTaskToDesk for displayId=%d, deskId=%d, taskId=%d, isVisible=%b",
+ displayId,
+ deskId,
+ taskId,
+ isVisible,
+ )
addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId)
addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId)
updateTaskInDesk(
@@ -265,6 +275,12 @@ class DesktopRepository(
}
private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) {
+ logD(
+ "addActiveTaskToDesk for displayId=%d, deskId=%d, taskId=%d",
+ displayId,
+ deskId,
+ taskId,
+ )
val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
// Removes task if it is active on another desk excluding this desk.
@@ -279,6 +295,7 @@ class DesktopRepository(
/** Removes task from active task list of desks excluding the [excludedDeskId]. */
@VisibleForTesting
fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) {
+ logD("removeActiveTask for taskId=%d, excludedDeskId=%d", taskId, excludedDeskId)
val affectedDisplays = mutableSetOf<Int>()
desktopData
.desksSequence()
@@ -303,6 +320,7 @@ class DesktopRepository(
taskId: Int,
notifyListeners: Boolean = true,
): Boolean {
+ logD("removeActiveTaskFromDesk for deskId=%d, taskId=%d", deskId, taskId)
val desk = desktopData.getDesk(deskId) ?: return false
if (desk.activeTasks.remove(taskId)) {
logD("Removed active task=%d from deskId=%d", taskId, desk.deskId)
@@ -314,29 +332,22 @@ class DesktopRepository(
return false
}
- /**
- * Adds given task to the closing task list for [displayId]'s active desk.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
- */
- fun addClosingTask(displayId: Int, taskId: Int) {
- val activeDesk =
- desktopData.getActiveDesk(displayId)
- ?: error("Expected active desk in display: $displayId")
- if (activeDesk.closingTasks.add(taskId)) {
- logD(
- "Added closing task=%d displayId=%d deskId=%d",
- taskId,
- displayId,
- activeDesk.deskId,
- )
+ /** Adds given task to the closing task list of its desk. */
+ fun addClosingTask(displayId: Int, deskId: Int?, taskId: Int) {
+ val desk =
+ deskId?.let { desktopData.getDesk(it) }
+ ?: checkNotNull(desktopData.getActiveDesk(displayId)) {
+ "Expected active desk in display: $displayId"
+ }
+ if (desk.closingTasks.add(taskId)) {
+ logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, desk.deskId)
} else {
// If the task hasn't been removed from closing list after it disappeared.
logW(
"Task with taskId=%d displayId=%d deskId=%d is already closing",
taskId,
displayId,
- activeDesk.deskId,
+ desk.deskId,
)
}
}
@@ -374,7 +385,8 @@ class DesktopRepository(
* Checks if a task is the only visible, non-closing, non-minimized task on the active desk of
* the given display, or any display's active desk if [displayId] is [INVALID_DISPLAY].
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - consider forcing callers to use [isOnlyVisibleNonClosingTaskInDesk] with
+ * an explicit desk id instead of using this function and defaulting to the active one.
*/
fun isOnlyVisibleNonClosingTask(taskId: Int, displayId: Int = INVALID_DISPLAY): Boolean {
val activeDesks =
@@ -384,14 +396,27 @@ class DesktopRepository(
desktopData.getAllActiveDesks()
}
return activeDesks.any { desk ->
- desk.visibleTasks
- .subtract(desk.closingTasks)
- .subtract(desk.minimizedTasks)
- .singleOrNull() == taskId
+ isOnlyVisibleNonClosingTaskInDesk(
+ taskId = taskId,
+ deskId = desk.deskId,
+ displayId = desk.displayId,
+ )
}
}
/**
+ * Checks if a task is the only visible, non-closing, non-minimized task on the given desk of
+ * the given display.
+ */
+ fun isOnlyVisibleNonClosingTaskInDesk(taskId: Int, deskId: Int, displayId: Int): Boolean {
+ val desk = desktopData.getDesk(deskId) ?: return false
+ return desk.visibleTasks
+ .subtract(desk.closingTasks)
+ .subtract(desk.minimizedTasks)
+ .singleOrNull() == taskId
+ }
+
+ /**
* Returns the active tasks in the given display's active desk.
*
* TODO: b/389960283 - migrate callers to [getActiveTaskIdsInDesk].
@@ -456,7 +481,7 @@ class DesktopRepository(
/** Removes task from visible tasks of all desks except [excludedDeskId]. */
private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) {
- desktopData.forAllDesks { displayId, desk ->
+ desktopData.forAllDesks { _, desk ->
if (desk.deskId != excludedDeskId) {
removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId)
}
@@ -668,6 +693,11 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun setTopTransparentFullscreenTaskId(displayId: Int, taskId: Int) {
+ logD(
+ "Top transparent fullscreen task set for display: taskId=%d, displayId=%d",
+ taskId,
+ displayId,
+ )
desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId = taskId
}
@@ -685,6 +715,11 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun clearTopTransparentFullscreenTaskId(displayId: Int) {
+ logD(
+ "Top transparent fullscreen task cleared for display: taskId=%d, displayId=%d",
+ desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId,
+ displayId,
+ )
desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId = null
}
@@ -718,6 +753,12 @@ class DesktopRepository(
* Unminimizes the task if it is minimized.
*/
private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) {
+ logD(
+ "addOrMoveTaskToTopOfDesk displayId=%d, deskId=%d, taskId=%d",
+ displayId,
+ deskId,
+ taskId,
+ )
val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId")
logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId)
desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) }
@@ -738,6 +779,7 @@ class DesktopRepository(
* desk id instead of using this function and defaulting to the active one.
*/
fun minimizeTask(displayId: Int, taskId: Int) {
+ logD("minimizeTask displayId=%d, taskId=%d", displayId, taskId)
if (displayId == INVALID_DISPLAY) {
// When a task vanishes it doesn't have a displayId. Find the display of the task and
// mark it as minimized.
@@ -756,7 +798,7 @@ class DesktopRepository(
/** Minimizes the task in its desk. */
@VisibleForTesting
fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) {
- logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
+ logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId)
?: logD("Minimize task: No active desk found for task: taskId=%d", taskId)
updateTaskInDesk(displayId, deskId, taskId, isVisible = false)
@@ -771,12 +813,12 @@ class DesktopRepository(
* TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead.
*/
fun unminimizeTask(displayId: Int, taskId: Int) {
- logD("Unminimize Task: display=%d, task=%d", displayId, taskId)
+ logD("UnminimizeTask: display=%d, task=%d", displayId, taskId)
desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) }
}
private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) {
- logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId)
+ logD("Unminimize Task from desk: deskId=%d, taskId=%d", deskId, taskId)
if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) {
logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId)
}
@@ -847,6 +889,7 @@ class DesktopRepository(
/** Removes the given desk and returns the active tasks in that desk. */
fun removeDesk(deskId: Int): Set<Int> {
+ logD("removeDesk %d", deskId)
val desk =
desktopData.getDesk(deskId)
?: return emptySet<Int>().also {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
index e831d5eecdc2..6034299453fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt
@@ -19,13 +19,16 @@ package com.android.wm.shell.desktopmode
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.window.DesktopModeFlags
+import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.freeform.TaskChangeListener
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
/** Manages tasks handling specific to Android Desktop Mode. */
class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUserRepositories) :
TaskChangeListener {
override fun onTaskOpening(taskInfo: RunningTaskInfo) {
+ logD("onTaskOpening for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId)
val desktopRepository: DesktopRepository =
desktopUserRepositories.getProfile(taskInfo.userId)
if (!isFreeformTask(taskInfo) && desktopRepository.isActiveTask(taskInfo.taskId)) {
@@ -38,6 +41,7 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser
}
override fun onTaskChanging(taskInfo: RunningTaskInfo) {
+ logD("onTaskChanging for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId)
val desktopRepository: DesktopRepository =
desktopUserRepositories.getProfile(taskInfo.userId)
if (!desktopRepository.isActiveTask(taskInfo.taskId)) return
@@ -67,9 +71,15 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser
// of race conditions and possible duplications with [onTaskChanging].
override fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) {
// TODO: b/367268953 - Propagate usages from FreeformTaskListener to this method.
+ logD(
+ "onNonTransitionTaskChanging for taskId=%d, displayId=%d",
+ taskInfo.taskId,
+ taskInfo.displayId,
+ )
}
override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) {
+ logD("onTaskMovingToFront for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId)
val desktopRepository: DesktopRepository =
desktopUserRepositories.getProfile(taskInfo.userId)
if (!desktopRepository.isActiveTask(taskInfo.taskId)) return
@@ -80,10 +90,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser
}
override fun onTaskMovingToBack(taskInfo: RunningTaskInfo) {
+ logD("onTaskMovingToBack for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId)
// TODO: b/367268953 - Connect this with DesktopRepository.
}
override fun onTaskClosing(taskInfo: RunningTaskInfo) {
+ logD("onTaskClosing for taskId=%d, displayId=%d", taskInfo.taskId, taskInfo.displayId)
val desktopRepository: DesktopRepository =
desktopUserRepositories.getProfile(taskInfo.userId)
if (!desktopRepository.isActiveTask(taskInfo.taskId)) return
@@ -104,4 +116,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser
private fun isFreeformTask(taskInfo: RunningTaskInfo): Boolean =
taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+
+ private fun logD(msg: String, vararg arguments: Any?) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ companion object {
+ private const val TAG = "DesktopTaskChangeListener"
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 93058db0c171..45adfe4112a6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -117,6 +117,7 @@ import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransitionState
import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING
+import com.android.wm.shell.shared.R as SharedR
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellDesktopThread
@@ -801,6 +802,9 @@ class DesktopTasksController(
): ((IBinder) -> Unit) {
val taskId = taskInfo.taskId
val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+ if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ error("Did not find desk for task: $taskId")
+ }
snapEventHandler.removeTaskIfTiled(displayId, taskId)
val shouldExitDesktop =
willExitDesktop(
@@ -818,7 +822,7 @@ class DesktopTasksController(
shouldEndUpAtHome = true,
)
- taskRepository.addClosingTask(displayId, taskId)
+ taskRepository.addClosingTask(displayId = displayId, deskId = deskId, taskId = taskId)
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId, taskId)
)
@@ -870,6 +874,10 @@ class DesktopTasksController(
private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
val taskId = taskInfo.taskId
val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
+ if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ logW("minimizeTaskInner: desk not found for task: ${taskInfo.taskId}")
+ return
+ }
val displayId = taskInfo.displayId
val wct = WindowContainerTransaction()
@@ -890,10 +898,26 @@ class DesktopTasksController(
taskInfo = taskInfo,
reason = DesktopImmersiveController.ExitReason.MINIMIZED,
)
-
- wct.reorder(taskInfo.token, false)
- val isLastTask = taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)
- val transition: IBinder =
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ desksOrganizer.minimizeTask(
+ wct = wct,
+ deskId = checkNotNull(deskId) { "Expected non-null deskId" },
+ task = taskInfo,
+ )
+ } else {
+ wct.reorder(taskInfo.token, /* onTop= */ false)
+ }
+ val isLastTask =
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ taskRepository.isOnlyVisibleNonClosingTaskInDesk(
+ taskId = taskId,
+ deskId = checkNotNull(deskId) { "Expected non-null deskId" },
+ displayId = displayId,
+ )
+ } else {
+ taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
+ }
+ val transition =
freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
desktopTasksLimiter.ifPresent {
it.addPendingMinimizeChange(
@@ -1231,9 +1255,9 @@ class DesktopTasksController(
// home.
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
performDesktopExitCleanupIfNeeded(
- task.taskId,
- task.displayId,
- wct,
+ taskId = task.taskId,
+ displayId = task.displayId,
+ wct = wct,
forceToFullscreen = false,
// TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing
// display focus. Remove shouldEndUpAtHome = false when home focus handling
@@ -1800,6 +1824,7 @@ class DesktopTasksController(
private fun performDesktopExitCleanupIfNeeded(
taskId: Int,
+ deskId: Int? = null,
displayId: Int,
wct: WindowContainerTransaction,
forceToFullscreen: Boolean,
@@ -1813,13 +1838,14 @@ class DesktopTasksController(
// |RunOnTransitStart| when the transition is started.
return performDesktopExitCleanUp(
wct = wct,
- deskId = null,
+ deskId = deskId,
displayId = displayId,
willExitDesktop = true,
shouldEndUpAtHome = shouldEndUpAtHome,
)
}
+ /** TODO: b/394268248 - update [deskId] to be non-null. */
private fun performDesktopExitCleanUp(
wct: WindowContainerTransaction,
deskId: Int?,
@@ -2015,7 +2041,9 @@ class DesktopTasksController(
}
val cornerRadius =
context.resources
- .getDimensionPixelSize(R.dimen.desktop_windowing_freeform_rounded_corner_radius)
+ .getDimensionPixelSize(
+ SharedR.dimen.desktop_windowing_freeform_rounded_corner_radius
+ )
.toFloat()
info.changes
.filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
@@ -2370,17 +2398,28 @@ class DesktopTasksController(
): WindowContainerTransaction? {
logV("handleTaskClosing")
if (!isDesktopModeShowing(task.displayId)) return null
+ val deskId = taskRepository.getDeskIdForTask(task.taskId)
+ if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ return null
+ }
val wct = WindowContainerTransaction()
- performDesktopExitCleanupIfNeeded(
- task.taskId,
- task.displayId,
- wct,
- forceToFullscreen = false,
- )
+ val deactivationRunnable =
+ performDesktopExitCleanupIfNeeded(
+ taskId = task.taskId,
+ deskId = deskId,
+ displayId = task.displayId,
+ wct = wct,
+ forceToFullscreen = false,
+ )
+ deactivationRunnable?.invoke(transition)
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
- taskRepository.addClosingTask(task.displayId, task.taskId)
+ taskRepository.addClosingTask(
+ displayId = task.displayId,
+ deskId = deskId,
+ taskId = task.taskId,
+ )
snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
}
@@ -2584,9 +2623,9 @@ class DesktopTasksController(
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
performDesktopExitCleanupIfNeeded(
- taskInfo.taskId,
- taskInfo.displayId,
- wct,
+ taskId = taskInfo.taskId,
+ displayId = taskInfo.displayId,
+ wct = wct,
forceToFullscreen = false,
shouldEndUpAtHome = false,
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl
index 59add47fc79d..5f45192569e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl
@@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode;
/**
* Defines the state of desks on a display whose ID is `displayId`, which is:
- * - `canCreateDesks`: whether it's possible to create new desks on this display.
* - `activeDeskId`: the currently active desk Id, or `-1` if none is active.
* - `deskId`: the list of desk Ids of the available desks on this display.
*/
parcelable DisplayDeskState {
int displayId;
- boolean canCreateDesk;
int activeDeskId;
int[] deskIds;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index b46051c51fcc..cb231800bd63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -43,7 +43,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
-import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.animation.PhysicsAnimator
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
@@ -120,10 +120,7 @@ sealed class DragToDesktopTransitionHandler(
dragToDesktopAnimator: MoveToDesktopAnimator,
) {
if (inProgress) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "DragToDesktop: Drag to desktop transition already in progress.",
- )
+ logV("Drag to desktop transition already in progress.")
return
}
@@ -537,12 +534,14 @@ sealed class DragToDesktopTransitionHandler(
state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
) {
+ logV("mergeAnimation: cancel through split")
clearState()
return
}
// In case of bubble animation, finish the initial desktop drag animation, but keep the
// current animation running and have bubbles take over
if (info.type == TRANSIT_CONVERT_TO_BUBBLE) {
+ logV("mergeAnimation: convert-to-bubble")
state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
clearState()
return
@@ -562,6 +561,7 @@ sealed class DragToDesktopTransitionHandler(
state.startTransitionFinishCb
?: error("Start transition expected to be waiting for merge but wasn't")
if (isEndTransition) {
+ logV("mergeAnimation: end-transition, target=$mergeTarget")
setupEndDragToDesktop(
info,
startTransaction = startT,
@@ -572,7 +572,10 @@ sealed class DragToDesktopTransitionHandler(
LatencyTracker.getInstance(context)
.onActionEnd(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
animateEndDragToDesktop(startTransaction = startT, startTransitionFinishCb)
- } else if (isCancelTransition) {
+ return
+ }
+ if (isCancelTransition) {
+ logV("mergeAnimation: cancel-transition, target=$mergeTarget")
LatencyTracker.getInstance(context)
.onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
info.changes.forEach { change ->
@@ -583,7 +586,9 @@ sealed class DragToDesktopTransitionHandler(
finishCallback.onTransitionFinished(/* wct= */ null)
startTransitionFinishCb.onTransitionFinished(/* wct= */ null)
clearState()
+ return
}
+ logW("unhandled merge transition: transitionInfo=$info")
}
protected open fun setupEndDragToDesktop(
@@ -724,10 +729,7 @@ sealed class DragToDesktopTransitionHandler(
return
}
if (state.startTransitionToken == transition) {
- ProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "DragToDesktop: onTransitionConsumed() start transition aborted",
- )
+ logV("onTransitionConsumed() start transition aborted")
state.startAborted = true
// The start-transition (DRAG_HOLD) is aborted, cancel its jank interaction.
interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
@@ -950,7 +952,16 @@ sealed class DragToDesktopTransitionHandler(
CANCEL_BUBBLE_RIGHT,
}
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ private fun logW(msg: String, vararg arguments: Any?) {
+ ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
companion object {
+ private const val TAG = "DragToDesktopTransitionHandler"
/** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
@@ -1052,10 +1063,7 @@ constructor(
val state = requireTransitionState()
val homeLeash = state.homeChange?.leash
if (homeLeash == null) {
- ProtoLog.e(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "DragToDesktop: home leash is null",
- )
+ logE("home leash is null")
} else {
// Hide home on finish to prevent flickering when wallpaper activity flag is enabled
finishTransaction.hide(homeLeash)
@@ -1096,6 +1104,12 @@ constructor(
val startBoundsWithOffset =
Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) }
+ logV(
+ "animateEndDragToDesktop: startBounds=$startBounds, endBounds=$endBounds, " +
+ "startScale=$startScale, startPosition=$startPosition, " +
+ "startBoundsWithOffset=$startBoundsWithOffset"
+ )
+
dragToDesktopStateListener?.onCommitToDesktopAnimationStart()
// Accept the merge by applying the merging transaction (applied by #showResizeVeil)
// and finish callback. Show the veil and position the task at the first frame before
@@ -1177,7 +1191,16 @@ constructor(
.start()
}
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ private fun logE(msg: String, vararg arguments: Any?) {
+ ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
companion object {
+ private const val TAG = "SpringDragToDesktopTransitionHandler"
/** The freeform tasks initial scale when committing the drag-to-desktop gesture. */
private val FREEFORM_TASKS_INITIAL_SCALE =
propertyValue("freeform_tasks_initial_scale", scale = 100f, default = 0.9f)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
index 7ed1581cdfdb..cefbd8947feb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
@@ -28,7 +28,7 @@ oneway interface IDesktopTaskListener {
* Called once when the listener first gets connected to initialize it with the current state of
* desks in Shell.
*/
- void onListenerConnected(in DisplayDeskState[] displayDeskStates);
+ void onListenerConnected(in DisplayDeskState[] displayDeskStates, boolean canCreateDesks);
/** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */
void onTasksVisibilityChanged(int displayId, int visibleTasksCount);
@@ -49,10 +49,10 @@ oneway interface IDesktopTaskListener {
void onExitDesktopModeTransitionStarted(int transitionDuration);
/**
- * Called when the conditions that allow the creation of a new desk on the display whose ID is
- * `displayId` changes to `canCreateDesks`. It's also called when a new display is added.
+ * Called when the conditions that allow the creation of a new desk changes. This is a global
+ * state for the entire device.
*/
- void onCanCreateDesksChanged(int displayId, boolean canCreateDesks);
+ void onCanCreateDesksChanged(boolean canCreateDesks);
/** Called when a desk whose ID is `deskId` is added to the display whose ID is `displayId`. */
void onDeskAdded(int displayId, int deskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
index 0f2f3711a9a3..fc359d7d67b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -40,9 +40,19 @@ interface DesksOrganizer {
task: ActivityManager.RunningTaskInfo,
)
+ /** Minimizes the given task of the given deskId. */
+ fun minimizeTask(
+ wct: WindowContainerTransaction,
+ deskId: Int,
+ task: ActivityManager.RunningTaskInfo,
+ )
+
/** Whether the change is for the given desk id. */
fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean
+ /** Whether the change is for a known desk. */
+ fun isDeskChange(change: TransitionInfo.Change): Boolean
+
/**
* Returns the desk id in which the task in the given change is located at the end of a
* transition, if any.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
index 339932cabd2c..f576258ebdaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -15,7 +15,9 @@
*/
package com.android.wm.shell.desktopmode.multidesks
+import android.annotation.SuppressLint
import android.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -25,6 +27,7 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.DesktopExperienceFlags
import android.window.TransitionInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.core.util.forEach
import com.android.internal.annotations.VisibleForTesting
@@ -43,8 +46,12 @@ class RootTaskDesksOrganizer(
private val shellTaskOrganizer: ShellTaskOrganizer,
) : DesksOrganizer, ShellTaskOrganizer.TaskListener {
- private val deskCreateRequests = mutableListOf<CreateRequest>()
- @VisibleForTesting val roots = SparseArray<DeskRoot>()
+ private val createDeskRootRequests = mutableListOf<CreateDeskRequest>()
+ @VisibleForTesting val deskRootsByDeskId = SparseArray<DeskRoot>()
+ private val createDeskMinimizationRootRequests =
+ mutableListOf<CreateDeskMinimizationRootRequest>()
+ @VisibleForTesting
+ val deskMinimizationRootsByDeskId: MutableMap<Int, DeskMinimizationRoot> = mutableMapOf()
init {
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
@@ -57,7 +64,7 @@ class RootTaskDesksOrganizer(
override fun createDesk(displayId: Int, callback: OnCreateCallback) {
logV("createDesk in display: %d", displayId)
- deskCreateRequests += CreateRequest(displayId, callback)
+ createDeskRootRequests += CreateDeskRequest(displayId, callback)
shellTaskOrganizer.createRootTask(
displayId,
WINDOWING_MODE_FREEFORM,
@@ -68,14 +75,14 @@ class RootTaskDesksOrganizer(
override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) {
logV("removeDesk %d", deskId)
- val desk = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
- wct.removeRootTask(desk.taskInfo.token)
+ deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
+ deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
}
override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) {
logV("activateDesk %d", deskId)
- val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
- wct.reorder(root.taskInfo.token, /* onTop= */ true)
+ val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
+ wct.reorder(root.token, /* onTop= */ true)
wct.setLaunchRoot(
/* container= */ root.taskInfo.token,
/* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
@@ -85,7 +92,7 @@ class RootTaskDesksOrganizer(
override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) {
logV("deactivateDesk %d", deskId)
- val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
+ val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
wct.setLaunchRoot(
/* container= */ root.taskInfo.token,
/* windowingModes= */ null,
@@ -98,16 +105,58 @@ class RootTaskDesksOrganizer(
deskId: Int,
task: RunningTaskInfo,
) {
- val root = roots[deskId] ?: error("Root not found for desk: $deskId")
+ val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId")
wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED)
wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
}
+ override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) {
+ val deskRoot =
+ checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
+ val minimizationRoot =
+ checkNotNull(deskMinimizationRootsByDeskId[deskId]) {
+ "Minimization root not found for desk: $deskId"
+ }
+ val taskId = task.taskId
+ if (taskId in minimizationRoot.children) {
+ logV("Task #$taskId is already minimized in desk #$deskId")
+ return
+ }
+ if (taskId !in deskRoot.children) {
+ logE("Attempted to minimize task=${task.taskId} in desk=$deskId but it was not a child")
+ return
+ }
+ wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true)
+ }
+
override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean =
- roots.contains(deskId) && change.taskInfo?.taskId == deskId
+ (isDeskRootChange(change) && change.taskId == deskId) ||
+ (getDeskMinimizationRootInChange(change)?.deskId == deskId)
+
+ override fun isDeskChange(change: TransitionInfo.Change): Boolean =
+ isDeskRootChange(change) || getDeskMinimizationRootInChange(change) != null
+
+ private fun isDeskRootChange(change: TransitionInfo.Change): Boolean =
+ change.taskId in deskRootsByDeskId
- override fun getDeskAtEnd(change: TransitionInfo.Change): Int? =
- change.taskInfo?.parentTaskId?.takeIf { it in roots }
+ private fun getDeskMinimizationRootInChange(
+ change: TransitionInfo.Change
+ ): DeskMinimizationRoot? =
+ deskMinimizationRootsByDeskId.values.find { it.rootId == change.taskId }
+
+ private val TransitionInfo.Change.taskId: Int
+ get() = taskInfo?.taskId ?: INVALID_TASK_ID
+
+ override fun getDeskAtEnd(change: TransitionInfo.Change): Int? {
+ val parentTaskId = change.taskInfo?.parentTaskId ?: return null
+ if (parentTaskId in deskRootsByDeskId) {
+ return parentTaskId
+ }
+ val deskMinimizationRoot =
+ deskMinimizationRootsByDeskId.values.find { root -> root.rootId == parentTaskId }
+ ?: return null
+ return deskMinimizationRoot.deskId
+ }
override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean =
change.taskInfo?.taskId == deskId &&
@@ -115,51 +164,176 @@ class RootTaskDesksOrganizer(
change.mode == TRANSIT_TO_FRONT
override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) {
- if (taskInfo.parentTaskId in roots) {
+ // Check whether this task is appearing inside a desk.
+ if (taskInfo.parentTaskId in deskRootsByDeskId) {
val deskId = taskInfo.parentTaskId
val taskId = taskInfo.taskId
logV("Task #$taskId appeared in desk #$deskId")
addChildToDesk(taskId = taskId, deskId = deskId)
return
}
- val deskId = taskInfo.taskId
- check(deskId !in roots) { "A root already exists for desk: $deskId" }
- val request =
- checkNotNull(deskCreateRequests.firstOrNull { it.displayId == taskInfo.displayId }) {
- "Task ${taskInfo.taskId} appeared without pending create request"
- }
- logV("Desk #$deskId appeared")
- roots[deskId] = DeskRoot(deskId, taskInfo, leash)
- deskCreateRequests.remove(request)
- request.onCreateCallback.onCreated(deskId)
+ // Check whether this task is appearing in a minimization root.
+ val minimizationRoot =
+ deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.parentTaskId }
+ if (minimizationRoot != null) {
+ val deskId = minimizationRoot.deskId
+ val taskId = taskInfo.taskId
+ logV("Task #$taskId was minimized in desk #$deskId ")
+ addChildToMinimizationRoot(taskId = taskId, deskId = deskId)
+ return
+ }
+ // The appearing task is a root (either a desk or a minimization root), it should not exist
+ // already.
+ check(taskInfo.taskId !in deskRootsByDeskId) {
+ "A root already exists for desk: ${taskInfo.taskId}"
+ }
+ check(deskMinimizationRootsByDeskId.values.none { it.rootId == taskInfo.taskId }) {
+ "A minimization root already exists with rootId: ${taskInfo.taskId}"
+ }
+
+ val appearingInDisplayId = taskInfo.displayId
+ // Check if there's any pending desk creation requests under this display.
+ val deskRequest =
+ createDeskRootRequests.firstOrNull { it.displayId == appearingInDisplayId }
+ if (deskRequest != null) {
+ // Appearing root matches desk request.
+ val deskId = taskInfo.taskId
+ logV("Desk #$deskId appeared")
+ deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash)
+ createDeskRootRequests.remove(deskRequest)
+ deskRequest.onCreateCallback.onCreated(deskId)
+ createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId)
+ return
+ }
+ // Check if there's any pending minimization container creation requests under this display.
+ val deskMinimizationRootRequest =
+ createDeskMinimizationRootRequests.first { it.displayId == appearingInDisplayId }
+ val deskId = deskMinimizationRootRequest.deskId
+ logV("Minimization container for desk #$deskId appeared with id=${taskInfo.taskId}")
+ val deskMinimizationRoot = DeskMinimizationRoot(deskId, taskInfo, leash)
+ deskMinimizationRootsByDeskId[deskId] = deskMinimizationRoot
+ createDeskMinimizationRootRequests.remove(deskMinimizationRootRequest)
+ hideMinimizationRoot(deskMinimizationRoot)
}
override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) {
- if (roots.contains(taskInfo.taskId)) {
+ if (deskRootsByDeskId.contains(taskInfo.taskId)) {
val deskId = taskInfo.taskId
- roots[deskId] = roots[deskId].copy(taskInfo = taskInfo)
+ deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo)
+ logV("Desk #$deskId's task info changed")
+ return
}
+ val minimizationRoot =
+ deskMinimizationRootsByDeskId.values.find { root -> root.rootId == taskInfo.taskId }
+ if (minimizationRoot != null) {
+ deskMinimizationRootsByDeskId.remove(minimizationRoot.deskId)
+ deskMinimizationRootsByDeskId[minimizationRoot.deskId] =
+ minimizationRoot.copy(taskInfo = taskInfo)
+ logV("Minimization root for desk#${minimizationRoot.deskId} task info changed")
+ return
+ }
+
+ val parentTaskId = taskInfo.parentTaskId
+ if (parentTaskId in deskRootsByDeskId) {
+ val deskId = taskInfo.parentTaskId
+ val taskId = taskInfo.taskId
+ logV("onTaskInfoChanged: Task #$taskId appeared in desk #$deskId")
+ addChildToDesk(taskId = taskId, deskId = deskId)
+ return
+ }
+ // Check whether this task is appearing in a minimization root.
+ val parentMinimizationRoot =
+ deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == parentTaskId }
+ if (parentMinimizationRoot != null) {
+ val deskId = parentMinimizationRoot.deskId
+ val taskId = taskInfo.taskId
+ logV("onTaskInfoChanged: Task #$taskId was minimized in desk #$deskId ")
+ addChildToMinimizationRoot(taskId = taskId, deskId = deskId)
+ return
+ }
+ logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}")
}
override fun onTaskVanished(taskInfo: RunningTaskInfo) {
- if (roots.contains(taskInfo.taskId)) {
+ if (deskRootsByDeskId.contains(taskInfo.taskId)) {
val deskId = taskInfo.taskId
- val deskRoot = roots[deskId]
+ val deskRoot = deskRootsByDeskId[deskId]
// Use the last saved taskInfo to obtain the displayId. Using the local one here will
// return -1 since the task is not unassociated with a display.
val displayId = deskRoot.taskInfo.displayId
logV("Desk #$deskId vanished from display #$displayId")
- roots.remove(deskId)
+ deskRootsByDeskId.remove(deskId)
+ return
+ }
+ val deskMinimizationRoot =
+ deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.taskId }
+ if (deskMinimizationRoot != null) {
+ logV("Minimization root for desk ${deskMinimizationRoot.deskId} vanished")
+ deskMinimizationRootsByDeskId.remove(deskMinimizationRoot.deskId)
return
}
+
+ // Check whether the vanishing task was a child of any desk.
// At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk,
// so search through each root to remove this if it's a child.
- roots.forEach { deskId, deskRoot ->
+ deskRootsByDeskId.forEach { deskId, deskRoot ->
if (deskRoot.children.remove(taskInfo.taskId)) {
logV("Task #${taskInfo.taskId} vanished from desk #$deskId")
return
}
}
+ // Check whether the vanishing task was a child of the minimized root and remove it.
+ deskMinimizationRootsByDeskId.values.forEach { root ->
+ val taskId = taskInfo.taskId
+ if (root.children.remove(taskId)) {
+ logV("Task #$taskId vanished from minimization root of desk #${root.deskId}")
+ return
+ }
+ }
+ }
+
+ private fun createDeskMinimizationRoot(displayId: Int, deskId: Int) {
+ createDeskMinimizationRootRequests +=
+ CreateDeskMinimizationRootRequest(displayId = displayId, deskId = deskId)
+ shellTaskOrganizer.createRootTask(
+ displayId,
+ WINDOWING_MODE_FREEFORM,
+ /* listener = */ this,
+ /* removeWithTaskOrganizer = */ true,
+ )
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun hideMinimizationRoot(root: DeskMinimizationRoot) {
+ shellTaskOrganizer.applyTransaction(
+ WindowContainerTransaction().apply { setHidden(root.token, /* hidden= */ true) }
+ )
+ }
+
+ private fun addChildToDesk(taskId: Int, deskId: Int) {
+ deskRootsByDeskId.forEach { _, deskRoot ->
+ if (deskRoot.deskId == deskId) {
+ deskRoot.children.add(taskId)
+ } else {
+ deskRoot.children.remove(taskId)
+ }
+ }
+ // A task cannot be in both a desk root and a minimization root at the same time, so make
+ // sure to remove them if needed.
+ deskMinimizationRootsByDeskId.values.forEach { root -> root.children.remove(taskId) }
+ }
+
+ private fun addChildToMinimizationRoot(taskId: Int, deskId: Int) {
+ deskMinimizationRootsByDeskId.forEach { _, minimizationRoot ->
+ if (minimizationRoot.deskId == deskId) {
+ minimizationRoot.children += taskId
+ } else {
+ minimizationRoot.children -= taskId
+ }
+ }
+ // A task cannot be in both a desk root and a minimization root at the same time, so make
+ // sure to remove them if needed.
+ deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId }
}
@VisibleForTesting
@@ -168,34 +342,55 @@ class RootTaskDesksOrganizer(
val taskInfo: RunningTaskInfo,
val leash: SurfaceControl,
val children: MutableSet<Int> = mutableSetOf(),
+ ) {
+ val token: WindowContainerToken = taskInfo.token
+ }
+
+ @VisibleForTesting
+ data class DeskMinimizationRoot(
+ val deskId: Int,
+ val taskInfo: RunningTaskInfo,
+ val leash: SurfaceControl,
+ val children: MutableSet<Int> = mutableSetOf(),
+ ) {
+ val rootId: Int
+ get() = taskInfo.taskId
+
+ val token: WindowContainerToken = taskInfo.token
+ }
+
+ private data class CreateDeskRequest(
+ val displayId: Int,
+ val onCreateCallback: OnCreateCallback,
)
+ private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int)
+
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ private fun logE(msg: String, vararg arguments: Any?) {
+ ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
override fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("$prefix$TAG")
pw.println("${innerPrefix}Desk Roots:")
- roots.forEach { deskId, root ->
+ deskRootsByDeskId.forEach { deskId, root ->
+ val minimizationRoot = deskMinimizationRootsByDeskId[deskId]
pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}")
+ pw.println("$innerPrefix displayId=${root.taskInfo.displayId}")
pw.println("$innerPrefix children=${root.children}")
- }
- }
-
- private fun addChildToDesk(taskId: Int, deskId: Int) {
- roots.forEach { _, deskRoot ->
- if (deskRoot.deskId == deskId) {
- deskRoot.children.add(taskId)
- } else {
- deskRoot.children.remove(taskId)
+ pw.println("$innerPrefix minimization root:")
+ pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}")
+ if (minimizationRoot != null) {
+ pw.println("$innerPrefix children=${minimizationRoot.children}")
}
}
}
- private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback)
-
- private fun logV(msg: String, vararg arguments: Any?) {
- ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
- }
-
companion object {
private const val TAG = "RootTaskDesksOrganizer"
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
index 3fad28ad232f..a98ae5566394 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md
@@ -9,6 +9,7 @@ particular order):
4) [Threading model in the Shell](threading.md)
5) [Making changes in the Shell](changes.md)
6) [Extending the Shell for Products/OEMs](extending.md)
+6) [Shell transitions](transitions.md)
7) [Debugging in the Shell](debugging.md)
8) [Testing in the Shell](testing.md)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md
new file mode 100644
index 000000000000..dc23bb0c77d7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md
@@ -0,0 +1,118 @@
+# Shell transitions
+[Back to home](README.md)
+
+---
+
+## General
+
+General guides for using Shell Transitions can be found here:
+- [Shell transitions animation guide](http://go/shell-transit-anim)
+- [Hitchhiker's guide to transitions](http://go/transitions-book)
+
+## Transient-launch transitions
+<span style="color:orange">Use with care!</span>
+
+Transient-launch transitions are a way to handle non-atomic (ie. gestural) transitions by allowing
+WM Core to put participating activities into a transiently visible or hidden state for the duration
+of the animation and adding the ability to cancel the transition.
+
+For example, if you are launching an activity normally, WM Core will be updated
+at the start of the animation which includes pausing the previous activity and resuming the next
+activity (and subsequently the transition will reconcile that state via an animation).
+
+If you are transiently launching an activity though, WM Core will ensure that both the leaving
+activity and the incoming activity will be RESUMED for the duration of the transition duration. In
+addition, WM Core will track the position of the transient-launch activity in the window hierarchy
+prior to the launch, and allow Shell to restore it to that position if the transitions needs to be
+canceled.
+
+Starting a transient-launch transition can be done via the activity options (since the activity may
+not have been started yet):
+```kotlin
+val opts = ActivityOptions.makeBasic().setTransientLaunch()
+val wct = WindowContainerTransaction()
+wct.sendPendingIntent(pendingIntent, new Intent(), opts.toBundle())
+transitions.startTransition(TRANSIT_OPEN, wct, ...)
+```
+
+And restoring the transient order via a WCT:
+```kotlin
+val wct = WindowContainerTransaction()
+wct.restoreTransientOrder(transientLaunchContainerToken)
+transitions.startTransition(TRANSIT_RESTORE, wct, ...)
+```
+
+### <span style="color:orange">Considerations</span>
+
+Usage of transient-launch transitions should be done with consideration, there are a few gotchas
+that might result in subtle and hard-to-reproduce issues.
+
+#### Understanding the flow
+When starting a transient-launch transition, there are several possible outcomes:
+1) The transition finishes as normal: The user is committing the transition to the state requested
+ at the start of the transition. In such cases, you can simply finish the transition and the
+ states of the transiently shown/hidden activities will be updated to match the original state
+ that a non-transient transition would have (ie. closing activities will be stopped).
+
+2) The transition is interrupted: A change in the system results in the window hierarchy changing
+ in a way which may or may not affect the transient-launch activity. eg. We transiently-launch
+ home from app A, but then app B launches. In this case, WM attempts to create a new transition
+ reflecting the window hierarchy changes (ie. if B occludes Home in the above example, then the
+ transition will have Home TO_BACK, and B TO_FRONT).
+
+ At this point, the transition handler can choose to merge the incoming transition or not (to
+ queue it after this current transition). Take note of the next section for concerns re. bookend
+ transitions.
+
+3) The transition is canceled: The user is canceling the transition to the previous state. In such
+ cases, you need to store the `WindowContainerToken` for the task associated with the
+ transient-launch activity, and restore the transient order via the `WindowContainerTransaction`
+ API above. In some cases, if anything has been reordered since (ie. due to other merged
+ transitions), then you may also want to use `WindowContainerTransaction#reorder()` to place all
+ the relevant containers to their original order (provided via the change-order in the initial
+ launch transition).
+
+#### Finishing the transient-launch transition
+
+When restoring the transient order in the 3rd flow above, it is recommended to do it in a new
+transition and <span style="color:orange">**not**</span> via the WindowContainerTransaction in
+`TransitionFinishCallback#onTransitionFinished()` provided when starting the transition.
+
+Changes to the window hierarchy via the finish transaction are not applied in sync with other
+transitions that are collecting and aplying, and are also not observable in Shell in any way.
+Starting a new transition instead ensures both. (The finish transaction can still be used if there
+are non-transition affecting properties (ie. container properties) that need to be updated as a part
+of finishing the transient-launch transition).
+
+So the general idea is when restoring is:
+
+1) Start transient-launch transition START_T
+2) ...
+3) All done, start bookend transition END_T
+4) Handler receives END_T, merges it and then finishes START_T
+
+In practice it's not quite that simple, due to the ordering of transitions and extra care must be
+taken when using a new transition to prevent deadlocking when merging transitions.
+
+When a new transition arrives while a transient-launch transition is playing, the handler can
+choose to handle/merge the transition into the ongoing one, or skip merging to queue it up to be
+played after. In the above flow, we can see how this might result in a deadlock:
+
+Queueing END during merge:
+1) Start transient-launch transition START_T
+2) ...
+3) Incoming transition OTHER_T, choose to cancel START_T -> start bookend transition END_T, but don't merge OTHER_T
+3) Waiting for END_T... <span style="color:red">Deadlock!</span>
+
+Interrupt while pending END:
+1) Start transient-launch transition START_T
+2) ...
+3) All done, start bookend transition END_T
+3) Incoming transition OTHER_T occurs before END_T, but don't merge OTHER_T
+3) Waiting for END_T... <span style="color:red">Deadlock!</span>
+
+This means that when using transient-launch transitions with a bookend transition
+<span style="color:orange">requires</span> you to handle any incoming transitions if the bookend is
+ever queued (or already posted) after it. You can do so by preempting the bookend transition
+(finishing the transient-launch transition), or handling the merge of the new transition (so it
+doesn't queue). \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 99c9302edb75..1ce24f76ada5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -404,6 +404,10 @@ public class PipController implements ConfigurationChangeListener,
mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, pictureInPictureParams,
mPipBoundsAlgorithm);
+
+ // Update the size spec in case aspect ratio is invariant, but display has changed
+ // since the last PiP session, or this is the first PiP session altogether.
+ mPipBoundsState.updateMinMaxSize(mPipBoundsState.getAspectRatio());
return mPipBoundsAlgorithm.getEntryDestinationBounds();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 035c93db7ee4..97b3e5a2da87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -738,6 +738,13 @@ public class PipTransition extends PipTransitionController implements
}
}
+ if (!mPipTransitionState.isInSwipePipToHomeTransition()) {
+ // Update the size spec in case aspect ratio is invariant, but display has changed
+ // since the last PiP session, or this is the first PiP session altogether.
+ // Skip the update if in swipe PiP to home, as this has already been done.
+ mPipBoundsState.updateMinMaxSize(mPipBoundsState.getAspectRatio());
+ }
+
// calculate the entry bounds and notify core to move task to pinned with final bounds
final Rect entryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
mPipBoundsState.setBounds(entryBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 847a0383e7d0..3e03e001c49b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -47,6 +47,7 @@ import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.IApplicationThread;
import android.app.PendingIntent;
+import android.app.WindowConfiguration;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
@@ -75,11 +76,11 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.Flags;
-import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.R;
import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.HomeTransitionObserver;
@@ -320,7 +321,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
"RecentsTransitionHandler.mergeAnimation: no controller found");
return;
}
- controller.merge(info, startT, finishT, mergeTarget, finishCallback);
+ controller.merge(info, startT, finishT, finishCallback);
}
@Override
@@ -408,7 +409,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
// next called.
private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel;
- // Used to track a pending finish transition
+ // Used to track a pending finish transition, this is only non-null if
+ // enableRecentsBookendTransition() is enabled
private IBinder mPendingFinishTransition;
private IResultReceiver mPendingRunnerFinishCb;
@@ -917,7 +919,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
*/
@SuppressLint("NewApi")
void merge(TransitionInfo info, SurfaceControl.Transaction startT,
- SurfaceControl.Transaction finishT, IBinder mergeTarget,
+ SurfaceControl.Transaction finishT,
Transitions.TransitionFinishCallback finishCallback) {
if (mFinishCB == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -927,16 +929,25 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
return;
}
- if (Flags.enableRecentsBookendTransition()
- && info.getType() == TRANSIT_END_RECENTS_TRANSITION
- && mergeTarget == mTransition) {
- // This is a pending finish, so merge the end transition to trigger completing the
- // cleanup of the recents transition
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION",
- mInstanceId);
- finishCallback.onTransitionFinished(null /* wct */);
- return;
+ if (Flags.enableRecentsBookendTransition()) {
+ if (info.getType() == TRANSIT_END_RECENTS_TRANSITION) {
+ // This is a pending finish, so merge the end transition to trigger completing
+ // the cleanup of the recents transition
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION",
+ mInstanceId);
+ consumeMerge(info, startT, finishT, finishCallback);
+ return;
+ } else if (mPendingFinishTransition != null) {
+ // This transition is interrupting a pending finish that was already sent, so
+ // pre-empt the pending finish transition since the state has already changed
+ // in the core
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: Awaiting TRANSIT_END_RECENTS_TRANSITION",
+ mInstanceId);
+ onFinishInner(null /* wct */);
+ return;
+ }
}
if (info.getType() == TRANSIT_SLEEP) {
@@ -1210,16 +1221,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
}
return;
}
+
// At this point, we are accepting the merge.
- startT.apply();
- // Since we're accepting the merge, update the finish transaction so that changes via
- // that transaction will be applied on top of those of the merged transitions
- mFinishTransaction = finishT;
+ consumeMerge(info, startT, finishT, finishCallback);
+
+ // Notify Launcher of the new opening tasks if necessary
boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue();
- if (!passTransitionInfo) {
- // not using the incoming anim-only surfaces
- info.releaseAnimSurfaces();
- }
if (appearedTargets != null) {
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
@@ -1229,6 +1236,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
}
}
+ }
+
+ /**
+ * Consumes the merge of the other given transition.
+ */
+ private void consumeMerge(TransitionInfo info, SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT,
+ Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.merge: consuming merge",
+ mInstanceId);
+
+ startT.apply();
+ // Since we're accepting the merge, update the finish transaction so that changes via
+ // that transaction will be applied on top of those of the merged transitions
+ mFinishTransaction = finishT;
+ boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue();
+ if (!passTransitionInfo) {
+ // not using the incoming anim-only surfaces
+ info.releaseAnimSurfaces();
+ }
finishCallback.onTransitionFinished(null /* wct */);
}
@@ -1346,9 +1374,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
final SurfaceControl.Transaction t = mFinishTransaction;
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ // The following code must set this if it is changing anything in core that might affect
+ // transitions as a part of finishing the recents transition
+ boolean requiresBookendTransition = false;
+
if (mKeyguardLocked && mRecentsTask != null) {
if (toHome) wct.reorder(mRecentsTask, true /* toTop */);
else wct.restoreTransientOrder(mRecentsTask);
+ // We are manipulating the window hierarchy, which should only be done with the
+ // bookend transition
+ requiresBookendTransition = true;
}
if (returningToApp) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " returning to app");
@@ -1365,6 +1400,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
if (!mKeyguardLocked && mRecentsTask != null) {
wct.restoreTransientOrder(mRecentsTask);
}
+ // We are manipulating the window hierarchy, which should only be done with the
+ // bookend transition
+ requiresBookendTransition = true;
} else if (toHome && mOpeningSeparateHome && mPausingTasks != null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " 3p launching home");
// Special situation where 3p launcher was changed during recents (this happens
@@ -1384,6 +1422,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
if (!mKeyguardLocked && mRecentsTask != null) {
wct.restoreTransientOrder(mRecentsTask);
}
+ // We are manipulating the window hierarchy, which should only be done with the
+ // bookend transition
+ requiresBookendTransition = true;
} else {
if (mPausingSeparateHome) {
if (mOpeningTasks.isEmpty()) {
@@ -1484,13 +1525,21 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
if (Flags.enableRecentsBookendTransition()) {
if (!wct.isEmpty()) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.finishInner: "
- + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId);
- mPendingRunnerFinishCb = runnerFinishCb;
- mPendingFinishTransition = mTransitions.startTransition(
- TRANSIT_END_RECENTS_TRANSITION, wct,
- new PendingFinishTransitionHandler());
+ if (requiresBookendTransition) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.finishInner: "
+ + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId);
+ mPendingRunnerFinishCb = runnerFinishCb;
+ mPendingFinishTransition = mTransitions.startTransition(
+ TRANSIT_END_RECENTS_TRANSITION, wct,
+ new PendingFinishTransitionHandler());
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.finishInner: Non-transition affecting wct",
+ mInstanceId);
+ mPendingRunnerFinishCb = runnerFinishCb;
+ onFinishInner(wct);
+ }
} else {
// If there's no work to do, just go ahead and clean up
mPendingRunnerFinishCb = runnerFinishCb;
@@ -1631,6 +1680,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] PendingFinishTransitionHandler.startAnimation: "
+ + "Started pending finish transition", mInstanceId);
return false;
}
@@ -1644,10 +1696,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler,
@Override
public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
@Nullable SurfaceControl.Transaction finishTransaction) {
+ if (mPendingFinishTransition == null) {
+ // The cleanup was pre-empted by an earlier transition, nothing there is nothing
+ // to do here
+ return;
+ }
// Once we have merged (or not if the WCT didn't result in any changes), then we can
// run the pending finish logic
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.onTransitionConsumed: "
+ "[%d] PendingFinishTransitionHandler.onTransitionConsumed: "
+ "Consumed pending finish transition", mInstanceId);
onFinishInner(null /* wct */);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index 1917996d48fb..938885cc1684 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -21,6 +21,7 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE;
import static com.android.wm.shell.transition.Transitions.TransitionObserver;
import android.annotation.NonNull;
@@ -35,6 +36,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.shared.IHomeTransitionListener;
import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
/**
* The {@link TransitionObserver} that observes for transitions involving the home
@@ -48,6 +50,8 @@ public class HomeTransitionObserver implements TransitionObserver,
private @NonNull final Context mContext;
private @NonNull final ShellExecutor mMainExecutor;
+ private Boolean mPendingHomeVisibilityUpdate;
+
public HomeTransitionObserver(@NonNull Context context,
@NonNull ShellExecutor mainExecutor) {
mContext = context;
@@ -59,28 +63,78 @@ public class HomeTransitionObserver implements TransitionObserver,
@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction) {
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ handleTransitionReadyWithBubbleAnything(info);
+ } else {
+ handleTransitionReady(info);
+ }
+ }
+
+ private void handleTransitionReady(@NonNull TransitionInfo info) {
for (TransitionInfo.Change change : info.getChanges()) {
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
- || taskInfo == null
+ if (taskInfo == null
+ || info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
|| taskInfo.displayId != DEFAULT_DISPLAY
|| taskInfo.taskId == -1
|| !taskInfo.isRunning) {
continue;
}
+ Boolean homeVisibilityUpdate = getHomeVisibilityUpdate(info, change, taskInfo);
+ if (homeVisibilityUpdate != null) {
+ notifyHomeVisibilityChanged(homeVisibilityUpdate);
+ }
+ }
+ }
+
+ private void handleTransitionReadyWithBubbleAnything(@NonNull TransitionInfo info) {
+ Boolean homeVisibilityUpdate = null;
+ for (TransitionInfo.Change change : info.getChanges()) {
+ final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+ if (taskInfo == null
+ || taskInfo.displayId != DEFAULT_DISPLAY
+ || taskInfo.taskId == -1
+ || !taskInfo.isRunning) {
+ continue;
+ }
+
+ Boolean update = getHomeVisibilityUpdate(info, change, taskInfo);
+ if (update != null) {
+ homeVisibilityUpdate = update;
+ }
+ }
+
+ if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP) {
+ // Do not apply at the start of desktop drag as that updates launcher UI visibility.
+ // Store the value and apply with a next transition if needed.
+ mPendingHomeVisibilityUpdate = homeVisibilityUpdate;
+ return;
+ }
+
+ if (info.getType() == TRANSIT_CONVERT_TO_BUBBLE && homeVisibilityUpdate == null) {
+ // We are converting to bubble and we did not get a change to home visibility in this
+ // transition. Apply the value from start of drag.
+ homeVisibilityUpdate = mPendingHomeVisibilityUpdate;
+ }
+ if (homeVisibilityUpdate != null) {
+ mPendingHomeVisibilityUpdate = null;
+ notifyHomeVisibilityChanged(homeVisibilityUpdate);
+ }
+ }
- final int mode = change.getMode();
- final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
- if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
- final boolean gestureToHomeTransition = isBackGesture
- && TransitionUtil.isClosingType(info.getType());
- if (gestureToHomeTransition || TransitionUtil.isClosingMode(mode)
- || (!isBackGesture && TransitionUtil.isOpeningMode(mode))) {
- notifyHomeVisibilityChanged(gestureToHomeTransition
- || TransitionUtil.isOpeningType(mode));
- }
+ private Boolean getHomeVisibilityUpdate(TransitionInfo info,
+ TransitionInfo.Change change, ActivityManager.RunningTaskInfo taskInfo) {
+ final int mode = change.getMode();
+ final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
+ if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
+ final boolean gestureToHomeTransition = isBackGesture
+ && TransitionUtil.isClosingType(info.getType());
+ if (gestureToHomeTransition || TransitionUtil.isClosingMode(mode)
+ || (!isBackGesture && TransitionUtil.isOpeningMode(mode))) {
+ return gestureToHomeTransition || TransitionUtil.isOpeningType(mode);
}
}
+ return null;
}
@Override
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 5a6ea214e561..cf139a008164 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
@@ -103,6 +103,7 @@ import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController;
import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -258,6 +259,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final RecentsTransitionHandler mRecentsTransitionHandler;
private final DesktopModeCompatPolicy mDesktopModeCompatPolicy;
private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel;
+ private final MultiDisplayDragMoveIndicatorController mMultiDisplayDragMoveIndicatorController;
public DesktopModeWindowDecorViewModel(
Context context,
@@ -296,7 +298,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
WindowDecorTaskResourceLoader taskResourceLoader,
RecentsTransitionHandler recentsTransitionHandler,
DesktopModeCompatPolicy desktopModeCompatPolicy,
- DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel,
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) {
this(
context,
shellExecutor,
@@ -340,7 +343,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
taskResourceLoader,
recentsTransitionHandler,
desktopModeCompatPolicy,
- desktopTilingDecorViewModel);
+ desktopTilingDecorViewModel,
+ multiDisplayDragMoveIndicatorController);
}
@VisibleForTesting
@@ -387,7 +391,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
WindowDecorTaskResourceLoader taskResourceLoader,
RecentsTransitionHandler recentsTransitionHandler,
DesktopModeCompatPolicy desktopModeCompatPolicy,
- DesktopTilingDecorViewModel desktopTilingDecorViewModel) {
+ DesktopTilingDecorViewModel desktopTilingDecorViewModel,
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -460,6 +465,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDesktopModeCompatPolicy = desktopModeCompatPolicy;
mDesktopTilingDecorViewModel = desktopTilingDecorViewModel;
mDesktopTasksController.setSnapEventHandler(this);
+ mMultiDisplayDragMoveIndicatorController = multiDisplayDragMoveIndicatorController;
shellInit.addInitCallback(this::onInit, this);
}
@@ -1759,7 +1765,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mTransitions,
mInteractionJankMonitor,
mTransactionFactory,
- mMainHandler);
+ mMainHandler,
+ mMultiDisplayDragMoveIndicatorController);
windowDecoration.setTaskDragResizer(taskPositioner);
final DesktopModeTouchEventListener touchEventListener =
@@ -2056,7 +2063,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
Transitions transitions,
InteractionJankMonitor interactionJankMonitor,
Supplier<SurfaceControl.Transaction> transactionFactory,
- Handler handler) {
+ Handler handler,
+ MultiDisplayDragMoveIndicatorController multiDisplayDragMoveIndicatorController) {
final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled()
// TODO(b/383632995): Update when the flag is launched.
? (Flags.enableConnectedDisplaysWindowDrag()
@@ -2067,7 +2075,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
dragEventListener,
transitions,
interactionJankMonitor,
- handler)
+ handler,
+ multiDisplayDragMoveIndicatorController)
: new VeiledResizeTaskPositioner(
taskOrganizer,
windowDecoration,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index dca376f7df0e..6165dbf686fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -1069,7 +1069,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private static int getCornerRadius(@NonNull Context context, int layoutResId) {
if (layoutResId == R.layout.desktop_mode_app_header) {
return loadDimensionPixelSize(context.getResources(),
- R.dimen.desktop_windowing_freeform_rounded_corner_radius);
+ com.android.wm.shell.shared.R.dimen
+ .desktop_windowing_freeform_rounded_corner_radius);
}
return INVALID_CORNER_RADIUS;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
index bb20292a51d4..c6cb62d153ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
@@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor
import android.graphics.PointF
import android.graphics.Rect
+import android.hardware.display.DisplayTopology
import android.os.Handler
import android.os.IBinder
import android.os.Looper
@@ -32,10 +33,10 @@ import com.android.internal.jank.InteractionJankMonitor
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator
+import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.transition.Transitions
import java.util.concurrent.TimeUnit
-import java.util.function.Supplier
/**
* A task positioner that also takes into account resizing a
@@ -49,11 +50,12 @@ class MultiDisplayVeiledResizeTaskPositioner(
private val desktopWindowDecoration: DesktopModeWindowDecoration,
private val displayController: DisplayController,
dragEventListener: DragPositioningCallbackUtility.DragEventListener,
- private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
+ private val transactionSupplier: () -> SurfaceControl.Transaction,
private val transitions: Transitions,
private val interactionJankMonitor: InteractionJankMonitor,
@ShellMainThread private val handler: Handler,
-) : TaskPositioner, Transitions.TransitionHandler {
+ private val multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController,
+) : TaskPositioner, Transitions.TransitionHandler, DisplayController.OnDisplaysChangedListener {
private val dragEventListeners =
mutableListOf<DragPositioningCallbackUtility.DragEventListener>()
private val stableBounds = Rect()
@@ -71,6 +73,7 @@ class MultiDisplayVeiledResizeTaskPositioner(
private var isResizingOrAnimatingResize = false
@Surface.Rotation private var rotation = 0
private var startDisplayId = 0
+ private val displayIds = mutableSetOf<Int>()
constructor(
taskOrganizer: ShellTaskOrganizer,
@@ -80,19 +83,22 @@ class MultiDisplayVeiledResizeTaskPositioner(
transitions: Transitions,
interactionJankMonitor: InteractionJankMonitor,
@ShellMainThread handler: Handler,
+ multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController,
) : this(
taskOrganizer,
windowDecoration,
displayController,
dragEventListener,
- Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() },
+ { SurfaceControl.Transaction() },
transitions,
interactionJankMonitor,
handler,
+ multiDisplayDragMoveIndicatorController,
)
init {
dragEventListeners.add(dragEventListener)
+ displayController.addDisplayWindowListener(this)
}
override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect {
@@ -164,7 +170,7 @@ class MultiDisplayVeiledResizeTaskPositioner(
createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW)
)
- val t = transactionSupplier.get()
+ val t = transactionSupplier()
val startDisplayLayout = displayController.getDisplayLayout(startDisplayId)
val currentDisplayLayout = displayController.getDisplayLayout(displayId)
@@ -196,7 +202,13 @@ class MultiDisplayVeiledResizeTaskPositioner(
)
)
- // TODO(b/383069173): Render drag indicator(s)
+ multiDisplayDragMoveIndicatorController.onDragMove(
+ boundsDp,
+ startDisplayId,
+ desktopWindowDecoration.mTaskInfo,
+ displayIds,
+ transactionSupplier,
+ )
t.setPosition(
desktopWindowDecoration.leash,
@@ -267,7 +279,10 @@ class MultiDisplayVeiledResizeTaskPositioner(
)
)
- // TODO(b/383069173): Clear drag indicator(s)
+ multiDisplayDragMoveIndicatorController.onDragEnd(
+ desktopWindowDecoration.mTaskInfo.taskId,
+ transactionSupplier,
+ )
}
interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW)
@@ -348,6 +363,14 @@ class MultiDisplayVeiledResizeTaskPositioner(
dragEventListeners.remove(dragEventListener)
}
+ override fun onTopologyChanged(topology: DisplayTopology) {
+ // TODO: b/383069173 - Cancel window drag when topology changes happen during drag.
+
+ displayIds.clear()
+ val displayBounds = topology.getAbsoluteBounds()
+ displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) })
+ }
+
companion object {
// Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid
// timing out in the middle of a resize or drag action.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt
new file mode 100644
index 000000000000..abd238847519
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiDisplayDragMoveIndicatorControllerTest.kt
@@ -0,0 +1,168 @@
+/*
+ * 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.wm.shell.common
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.graphics.RectF
+import android.testing.TestableResources
+import android.view.Display
+import android.view.SurfaceControl
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import java.util.function.Supplier
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [MultiDisplayDragMoveIndicatorController].
+ *
+ * Build/Install/Run: atest WMShellUnitTests:MultiDisplayDragMoveIndicatorControllerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MultiDisplayDragMoveIndicatorControllerTest : ShellTestCase() {
+ private val displayController = mock<DisplayController>()
+ private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
+ private val indicatorSurfaceFactory = mock<MultiDisplayDragMoveIndicatorSurface.Factory>()
+ private val indicatorSurface0 = mock<MultiDisplayDragMoveIndicatorSurface>()
+ private val indicatorSurface1 = mock<MultiDisplayDragMoveIndicatorSurface>()
+ private val transaction = mock<SurfaceControl.Transaction>()
+ private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>()
+ private val taskInfo = mock<RunningTaskInfo>()
+ private val display0 = mock<Display>()
+ private val display1 = mock<Display>()
+
+ private lateinit var resources: TestableResources
+ private val executor = TestShellExecutor()
+
+ private lateinit var controller: MultiDisplayDragMoveIndicatorController
+
+ @Before
+ fun setUp() {
+ resources = mContext.getOrCreateTestableResources()
+ val resourceConfiguration = Configuration()
+ resourceConfiguration.uiMode = 0
+ resources.overrideConfiguration(resourceConfiguration)
+
+ controller =
+ MultiDisplayDragMoveIndicatorController(
+ displayController,
+ rootTaskDisplayAreaOrganizer,
+ indicatorSurfaceFactory,
+ executor,
+ )
+
+ val spyDisplayLayout0 =
+ MultiDisplayTestUtil.createSpyDisplayLayout(
+ MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_0,
+ MultiDisplayTestUtil.DISPLAY_DPI_0,
+ resources.resources,
+ )
+ val spyDisplayLayout1 =
+ MultiDisplayTestUtil.createSpyDisplayLayout(
+ MultiDisplayTestUtil.DISPLAY_GLOBAL_BOUNDS_1,
+ MultiDisplayTestUtil.DISPLAY_DPI_1,
+ resources.resources,
+ )
+
+ taskInfo.taskId = TASK_ID
+ whenever(displayController.getDisplayLayout(0)).thenReturn(spyDisplayLayout0)
+ whenever(displayController.getDisplayLayout(1)).thenReturn(spyDisplayLayout1)
+ whenever(displayController.getDisplay(0)).thenReturn(display0)
+ whenever(displayController.getDisplay(1)).thenReturn(display1)
+ whenever(indicatorSurfaceFactory.create(taskInfo, display0)).thenReturn(indicatorSurface0)
+ whenever(indicatorSurfaceFactory.create(taskInfo, display1)).thenReturn(indicatorSurface1)
+ whenever(transactionSupplier.get()).thenReturn(transaction)
+ }
+
+ @Test
+ fun onDrag_boundsNotIntersectWithDisplay_noIndicator() {
+ controller.onDragMove(
+ RectF(2000f, 2000f, 2100f, 2200f), // not intersect with any display
+ startDisplayId = 0,
+ taskInfo,
+ displayIds = setOf(0, 1),
+ ) { transaction }
+ executor.flushAll()
+
+ verify(indicatorSurfaceFactory, never()).create(any(), any())
+ }
+
+ @Test
+ fun onDrag_boundsIntersectWithStartDisplay_noIndicator() {
+ controller.onDragMove(
+ RectF(100f, 100f, 200f, 200f), // intersect with display 0
+ startDisplayId = 0,
+ taskInfo,
+ displayIds = setOf(0, 1),
+ ) { transaction }
+ executor.flushAll()
+
+ verify(indicatorSurfaceFactory, never()).create(any(), any())
+ }
+
+ @Test
+ fun onDrag_boundsIntersectWithNonStartDisplay_showAndDisposeIndicator() {
+ controller.onDragMove(
+ RectF(100f, -100f, 200f, 200f), // intersect with display 0 and 1
+ startDisplayId = 0,
+ taskInfo,
+ displayIds = setOf(0, 1),
+ ) { transaction }
+ executor.flushAll()
+
+ verify(indicatorSurfaceFactory, times(1)).create(taskInfo, display1)
+ verify(indicatorSurface1, times(1))
+ .show(transaction, taskInfo, rootTaskDisplayAreaOrganizer, 1, Rect(0, 1800, 200, 2400))
+
+ controller.onDragMove(
+ RectF(2000f, 2000f, 2100f, 2200f), // not intersect with display 1
+ startDisplayId = 0,
+ taskInfo,
+ displayIds = setOf(0, 1)
+ ) { transaction }
+ while (executor.callbacks.isNotEmpty()) {
+ executor.flushAll()
+ }
+
+ verify(indicatorSurface1, times(1))
+ .relayout(any(), eq(transaction), shouldBeVisible = eq(false))
+
+ controller.onDragEnd(TASK_ID, { transaction })
+ while (executor.callbacks.isNotEmpty()) {
+ executor.flushAll()
+ }
+
+ verify(indicatorSurface1, times(1)).disposeSurface(transaction)
+ }
+
+ companion object {
+ private const val TASK_ID = 10
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 0d5741fccbcc..8ad54f5a0bb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -16,51 +16,28 @@
package com.android.wm.shell.desktopmode
-import android.app.ActivityManager.RunningTaskInfo
-import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
-import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
-import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
-import android.content.ContentResolver
-import android.os.Binder
import android.platform.test.annotations.EnableFlags
-import android.provider.Settings
-import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
-import android.view.IWindowManager
-import android.view.WindowManager.TRANSIT_CHANGE
-import android.window.DisplayAreaInfo
-import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.StaticMockitoSession
import com.android.window.flags.Flags
-import com.android.wm.shell.MockToken
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer
-import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
-import com.android.wm.shell.transition.Transitions
-import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.spy
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
-import org.mockito.kotlin.eq
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@@ -73,27 +50,18 @@ import org.mockito.quality.Strictness
@RunWith(AndroidTestingRunner::class)
class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Mock lateinit var testExecutor: ShellExecutor
- @Mock lateinit var transitions: Transitions
@Mock lateinit var displayController: DisplayController
- @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
- @Mock private lateinit var mockWindowManager: IWindowManager
@Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
@Mock private lateinit var mockDesktopRepository: DesktopRepository
@Mock private lateinit var mockDesktopTasksController: DesktopTasksController
- @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ @Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var shellInit: ShellInit
private lateinit var handler: DesktopDisplayEventHandler
private val onDisplaysChangedListenerCaptor = argumentCaptor<OnDisplaysChangedListener>()
- private val runningTasks = mutableListOf<RunningTaskInfo>()
private val externalDisplayId = 100
- private val freeformTask =
- TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()
- private val fullscreenTask =
- TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build()
- private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
@Before
fun setUp() {
@@ -105,24 +73,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
shellInit = spy(ShellInit(testExecutor))
whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository)
- whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
- whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
- .thenReturn(defaultTDA)
handler =
DesktopDisplayEventHandler(
context,
shellInit,
- transitions,
displayController,
- rootTaskDisplayAreaOrganizer,
- mockWindowManager,
mockDesktopUserRepositories,
mockDesktopTasksController,
- shellTaskOrganizer,
+ desktopDisplayModeController,
)
- whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
- runningTasks.add(freeformTask)
- runningTasks.add(fullscreenTask)
shellInit.init()
verify(displayController)
.addDisplayWindowListener(onDisplaysChangedListenerCaptor.capture())
@@ -133,65 +92,6 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
mockitoSession.finishMocking()
}
- private fun testDisplayWindowingModeSwitch(
- defaultWindowingMode: Int,
- extendedDisplayEnabled: Boolean,
- expectTransition: Boolean,
- ) {
- defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
- whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode }
- val settingsSession =
- ExtendedDisplaySettingsSession(
- context.contentResolver,
- if (extendedDisplayEnabled) 1 else 0,
- )
-
- settingsSession.use {
- connectExternalDisplay()
- defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- disconnectExternalDisplay()
-
- if (expectTransition) {
- val arg = argumentCaptor<WindowContainerTransaction>()
- verify(transitions, times(2))
- .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
- assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
- .isEqualTo(defaultWindowingMode)
- } else {
- verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull())
- }
- }
- }
-
- @Test
- fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() {
- testDisplayWindowingModeSwitch(
- defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
- extendedDisplayEnabled = false,
- expectTransition = false,
- )
- }
-
- @Test
- fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() {
- testDisplayWindowingModeSwitch(
- defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
- extendedDisplayEnabled = true,
- expectTransition = true,
- )
- }
-
- @Test
- fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() {
- testDisplayWindowingModeSwitch(
- defaultWindowingMode = WINDOWING_MODE_FREEFORM,
- extendedDisplayEnabled = true,
- expectTransition = false,
- )
- }
-
@Test
fun testDisplayAdded_supportsDesks_createsDesk() {
whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
@@ -231,70 +131,14 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
}
@Test
- fun displayWindowingModeSwitch_existingTasksOnConnected() {
- defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
- whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer {
- WINDOWING_MODE_FULLSCREEN
- }
-
- ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
- connectExternalDisplay()
-
- val arg = argumentCaptor<WindowContainerTransaction>()
- verify(transitions, times(1))
- .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
- assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED)
- assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
- }
- }
-
- @Test
- fun displayWindowingModeSwitch_existingTasksOnDisconnected() {
- defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer {
- WINDOWING_MODE_FULLSCREEN
- }
-
- ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
- disconnectExternalDisplay()
-
- val arg = argumentCaptor<WindowContainerTransaction>()
- verify(transitions, times(1))
- .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
- assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED)
- }
- }
-
- private fun connectExternalDisplay() {
- whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
- .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId))
+ fun testConnectExternalDisplay() {
onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId)
+ verify(desktopDisplayModeController).refreshDisplayWindowingMode()
}
- private fun disconnectExternalDisplay() {
- whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
- .thenReturn(intArrayOf(DEFAULT_DISPLAY))
+ @Test
+ fun testDisconnectExternalDisplay() {
onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId)
- }
-
- private class ExtendedDisplaySettingsSession(
- private val contentResolver: ContentResolver,
- private val overrideValue: Int,
- ) : AutoCloseable {
- private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
- private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0)
-
- init {
- Settings.Global.putInt(contentResolver, settingName, overrideValue)
- }
-
- override fun close() {
- Settings.Global.putInt(contentResolver, settingName, initialValue)
- }
+ verify(desktopDisplayModeController).refreshDisplayWindowingMode()
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
new file mode 100644
index 000000000000..0ff7230f6e0c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.content.ContentResolver
+import android.os.Binder
+import android.provider.Settings
+import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.IWindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DisplayAreaInfo
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.never
+import com.android.wm.shell.MockToken
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for [DesktopDisplayModeController]
+ *
+ * Usage: atest WMShellUnitTests:DesktopDisplayModeControllerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopDisplayModeControllerTest : ShellTestCase() {
+ private val transitions = mock<Transitions>()
+ private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
+ private val mockWindowManager = mock<IWindowManager>()
+ private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
+ private val desktopWallpaperActivityTokenProvider =
+ mock<DesktopWallpaperActivityTokenProvider>()
+
+ private lateinit var controller: DesktopDisplayModeController
+
+ private val runningTasks = mutableListOf<RunningTaskInfo>()
+ private val freeformTask =
+ TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()
+ private val fullscreenTask =
+ TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FULLSCREEN).build()
+ private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ private val wallpaperToken = MockToken().token()
+
+ @Before
+ fun setUp() {
+ whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder())
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .thenReturn(defaultTDA)
+ controller =
+ DesktopDisplayModeController(
+ context,
+ transitions,
+ rootTaskDisplayAreaOrganizer,
+ mockWindowManager,
+ shellTaskOrganizer,
+ desktopWallpaperActivityTokenProvider,
+ )
+ runningTasks.add(freeformTask)
+ runningTasks.add(fullscreenTask)
+ whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks))
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
+ }
+
+ private fun testDisplayWindowingModeSwitch(
+ defaultWindowingMode: Int,
+ extendedDisplayEnabled: Boolean,
+ expectTransition: Boolean,
+ ) {
+ defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
+ val settingsSession =
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (extendedDisplayEnabled) 1 else 0,
+ )
+
+ settingsSession.use {
+ connectExternalDisplay()
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ disconnectExternalDisplay()
+
+ if (expectTransition) {
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(transitions, times(2))
+ .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+ assertThat(arg.firstValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
+ .isEqualTo(defaultWindowingMode)
+ assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ } else {
+ verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull())
+ }
+ }
+ }
+
+ @Test
+ fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() {
+ testDisplayWindowingModeSwitch(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ extendedDisplayEnabled = false,
+ expectTransition = false,
+ )
+ }
+
+ @Test
+ fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() {
+ testDisplayWindowingModeSwitch(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ extendedDisplayEnabled = true,
+ expectTransition = true,
+ )
+ }
+
+ @Test
+ fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() {
+ testDisplayWindowingModeSwitch(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ extendedDisplayEnabled = true,
+ expectTransition = false,
+ )
+ }
+
+ @Test
+ fun displayWindowingModeSwitch_existingTasksOnConnected() {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN)
+
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
+ connectExternalDisplay()
+
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(transitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+ assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+ }
+
+ @Test
+ fun displayWindowingModeSwitch_existingTasksOnDisconnected() {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer {
+ WINDOWING_MODE_FULLSCREEN
+ }
+
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
+ disconnectExternalDisplay()
+
+ val arg = argumentCaptor<WindowContainerTransaction>()
+ verify(transitions, times(1))
+ .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
+ assertThat(arg.firstValue.changes[freeformTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ assertThat(arg.firstValue.changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+ }
+
+ private fun connectExternalDisplay() {
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, EXTERNAL_DISPLAY_ID))
+ controller.refreshDisplayWindowingMode()
+ }
+
+ private fun disconnectExternalDisplay() {
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayIds())
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY))
+ controller.refreshDisplayWindowingMode()
+ }
+
+ private class ExtendedDisplaySettingsSession(
+ private val contentResolver: ContentResolver,
+ private val overrideValue: Int,
+ ) : AutoCloseable {
+ private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+ private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0)
+
+ init {
+ Settings.Global.putInt(contentResolver, settingName, overrideValue)
+ }
+
+ override fun close() {
+ Settings.Global.putInt(contentResolver, settingName, initialValue)
+ }
+ }
+
+ private companion object {
+ const val EXTERNAL_DISPLAY_ID = 100
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index ed9b97d264f7..9bff287e314a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -333,7 +333,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
@Test
fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() {
repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
- repo.addClosingTask(DEFAULT_DISPLAY, 1)
+ repo.addClosingTask(displayId = DEFAULT_DISPLAY, deskId = 0, taskId = 1)
// A visible task that's closing
assertThat(repo.isVisibleTask(1)).isTrue()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index fcd92ac2678a..2e63c4f51792 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -2827,7 +2827,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
fun onDesktopWindowClose_singleActiveTask_isClosing() {
val task = setUpFreeformTask()
- taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId)
+ taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, deskId = 0, taskId = task.taskId)
val wct = WindowContainerTransaction()
controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task)
@@ -2864,7 +2864,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
- taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId)
+ taskRepository.addClosingTask(
+ displayId = DEFAULT_DISPLAY,
+ deskId = 0,
+ taskId = task2.taskId,
+ )
val wct = WindowContainerTransaction()
controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1)
@@ -3225,6 +3229,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onDesktopWindowMinimize_minimizesTask() {
+ val task = setUpFreeformTask()
+ val transition = Binder()
+ val runOnTransit = RunOnStartTransitionCallback()
+ whenever(
+ freeformTaskTransitionStarter.startMinimizedModeTransition(
+ any(),
+ anyInt(),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(transition)
+ whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any()))
+ .thenReturn(
+ ExitResult.Exit(exitingTask = task.taskId, runOnTransitionStart = runOnTransit)
+ )
+
+ controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)
+
+ verify(desksOrganizer).minimizeTask(any(), /* deskId= */ eq(0), eq(task))
+ }
+
+ @Test
fun onDesktopWindowMinimize_triesToStopTiling() {
val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val transition = Binder()
@@ -3972,7 +4000,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ taskRepository.addClosingTask(
+ displayId = DEFAULT_DISPLAY,
+ deskId = 0,
+ taskId = task2.taskId,
+ )
val result =
controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
@@ -4083,6 +4115,36 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_closeTransition_onlyDesktopTask_deactivatesDesk() {
+ val task = setUpFreeformTask()
+
+ controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ verify(desksOrganizer).deactivateDesk(any(), /* deskId= */ eq(0))
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_closeTransition_onlyDesktopTask_addsDeactivatesDeskTransition() {
+ val transition = Binder()
+ val task = setUpFreeformTask()
+
+ controller.handleRequest(transition, createTransition(task, type = TRANSIT_CLOSE))
+
+ verify(desksTransitionsObserver)
+ .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0))
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() {
val task1 = setUpFreeformTask()
@@ -4115,7 +4177,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ taskRepository.addClosingTask(
+ displayId = DEFAULT_DISPLAY,
+ deskId = 0,
+ taskId = task2.taskId,
+ )
val result =
controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
index 8b10ca1a2a70..96b85ad2729e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -22,6 +22,7 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.Change
import android.window.WindowContainerTransaction.HierarchyOp
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT
import androidx.test.filters.SmallTest
@@ -29,15 +30,19 @@ import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
+import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskMinimizationRoot
import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
/**
@@ -75,6 +80,43 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
+ fun testCreateDesk_createsMinimizationRoot() {
+ val callback = FakeOnCreateCallback()
+ organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+
+ val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]
+ assertNotNull(minimizationRoot)
+ assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId)
+ assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId)
+ }
+
+ @Test
+ fun testCreateMinimizationRoot_marksHidden() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+
+ verify(mockShellTaskOrganizer)
+ .applyTransaction(
+ argThat { wct ->
+ wct.changes.any { change ->
+ change.key == minimizationRootTask.token.asBinder() &&
+ (change.value.changeMask and Change.CHANGE_HIDDEN != 0) &&
+ change.value.hidden
+ }
+ }
+ )
+ }
+
+ @Test
fun testOnTaskAppeared_withoutRequest_throws() {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
@@ -105,57 +147,122 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
+ fun testOnTaskAppeared_duplicateMinimizedRoot_throws() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+
+ assertThrows(Exception::class.java) {
+ organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ }
+ }
+
+ @Test
fun testOnTaskVanished_removesRoot() {
val desk = createDesk()
- organizer.onTaskVanished(desk.taskInfo)
+ organizer.onTaskVanished(desk.deskRoot.taskInfo)
+
+ assertThat(organizer.deskRootsByDeskId.contains(desk.deskRoot.deskId)).isFalse()
+ }
+
+ @Test
+ fun testOnTaskVanished_removesMinimizedRoot() {
+ val desk = createDesk()
+
+ organizer.onTaskVanished(desk.deskRoot.taskInfo)
+ organizer.onTaskVanished(desk.minimizationRoot.taskInfo)
- assertThat(organizer.roots.contains(desk.deskId)).isFalse()
+ assertThat(organizer.deskMinimizationRootsByDeskId.contains(desk.deskRoot.deskId)).isFalse()
}
@Test
fun testDesktopWindowAppearsInDesk() {
val desk = createDesk()
- val child = createFreeformTask().apply { parentTaskId = desk.deskId }
+ val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
- assertThat(desk.children).contains(child.taskId)
+ assertThat(desk.deskRoot.children).contains(child.taskId)
+ }
+
+ @Test
+ fun testDesktopWindowAppearsInDeskMinimizationRoot() {
+ val desk = createDesk()
+ val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
+
+ organizer.onTaskAppeared(child, SurfaceControl())
+
+ assertThat(desk.minimizationRoot.children).contains(child.taskId)
+ }
+
+ @Test
+ fun testDesktopWindowMovesToMinimizationRoot() {
+ val desk = createDesk()
+ val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ organizer.onTaskAppeared(child, SurfaceControl())
+
+ child.parentTaskId = desk.minimizationRoot.rootId
+ organizer.onTaskInfoChanged(child)
+
+ assertThat(desk.deskRoot.children).doesNotContain(child.taskId)
+ assertThat(desk.minimizationRoot.children).contains(child.taskId)
}
@Test
fun testDesktopWindowDisappearsFromDesk() {
val desk = createDesk()
- val child = createFreeformTask().apply { parentTaskId = desk.deskId }
+ val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
organizer.onTaskVanished(child)
- assertThat(desk.children).doesNotContain(child.taskId)
+ assertThat(desk.deskRoot.children).doesNotContain(child.taskId)
}
@Test
- fun testRemoveDesk() {
+ fun testDesktopWindowDisappearsFromDeskMinimizationRoot() {
+ val desk = createDesk()
+ val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
+
+ organizer.onTaskAppeared(child, SurfaceControl())
+ organizer.onTaskVanished(child)
+
+ assertThat(desk.minimizationRoot.children).doesNotContain(child.taskId)
+ }
+
+ @Test
+ fun testRemoveDesk_removesDeskRoot() {
val desk = createDesk()
val wct = WindowContainerTransaction()
- organizer.removeDesk(wct, desk.deskId)
+ organizer.removeDesk(wct, desk.deskRoot.deskId)
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK &&
- hop.container == desk.taskInfo.token.asBinder()
+ hop.container == desk.deskRoot.token.asBinder()
}
)
.isTrue()
}
@Test
- fun testRemoveDesk_didNotExist_throws() {
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ fun testRemoveDesk_removesMinimizationRoot() {
+ val desk = createDesk()
val wct = WindowContainerTransaction()
- assertThrows(Exception::class.java) { organizer.removeDesk(wct, freeformRoot.taskId) }
+ organizer.removeDesk(wct, desk.deskRoot.deskId)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK &&
+ hop.container == desk.minimizationRoot.token.asBinder()
+ }
+ )
+ .isTrue()
}
@Test
@@ -163,20 +270,20 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
val desk = createDesk()
val wct = WindowContainerTransaction()
- organizer.activateDesk(wct, desk.deskId)
+ organizer.activateDesk(wct, desk.deskRoot.deskId)
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER &&
hop.toTop &&
- hop.container == desk.taskInfo.token.asBinder()
+ hop.container == desk.deskRoot.taskInfo.token.asBinder()
}
)
.isTrue()
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
- hop.container == desk.taskInfo.token.asBinder()
+ hop.container == desk.deskRoot.taskInfo.token.asBinder()
}
)
.isTrue()
@@ -196,14 +303,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
val wct = WindowContainerTransaction()
- organizer.moveTaskToDesk(wct, desk.deskId, desktopTask)
+ organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, desktopTask)
assertThat(
wct.hierarchyOps.any { hop ->
hop.isReparent &&
hop.toTop &&
hop.container == desktopTask.token.asBinder() &&
- hop.newParent == desk.taskInfo.token.asBinder()
+ hop.newParent == desk.deskRoot.taskInfo.token.asBinder()
}
)
.isTrue()
@@ -231,13 +338,26 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun testGetDeskAtEnd() {
val desk = createDesk()
- val task = createFreeformTask().apply { parentTaskId = desk.deskId }
+ val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ val endDesk =
+ organizer.getDeskAtEnd(
+ TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
+ )
+
+ assertThat(endDesk).isEqualTo(desk.deskRoot.deskId)
+ }
+
+ @Test
+ fun testGetDeskAtEnd_inMinimizationRoot() {
+ val desk = createDesk()
+
+ val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
val endDesk =
organizer.getDeskAtEnd(
TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
)
- assertThat(endDesk).isEqualTo(desk.deskId)
+ assertThat(endDesk).isEqualTo(desk.deskRoot.deskId)
}
@Test
@@ -264,14 +384,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
fun deactivateDesk_clearsLaunchRoot() {
val wct = WindowContainerTransaction()
val desk = createDesk()
- organizer.activateDesk(wct, desk.deskId)
+ organizer.activateDesk(wct, desk.deskRoot.deskId)
- organizer.deactivateDesk(wct, desk.deskId)
+ organizer.deactivateDesk(wct, desk.deskRoot.deskId)
assertThat(
wct.hierarchyOps.any { hop ->
hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT &&
- hop.container == desk.taskInfo.token.asBinder() &&
+ hop.container == desk.deskRoot.taskInfo.token.asBinder() &&
hop.windowingModes == null &&
hop.activityTypes == null
}
@@ -280,25 +400,129 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange() {
+ fun isDeskChange_forDeskId() {
val desk = createDesk()
assertThat(
organizer.isDeskChange(
- TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply {
- taskInfo = desk.taskInfo
+ TransitionInfo.Change(desk.deskRoot.taskInfo.token, desk.deskRoot.leash).apply {
+ taskInfo = desk.deskRoot.taskInfo
},
- desk.deskId,
+ desk.deskRoot.deskId,
+ )
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun isDeskChange_forDeskId_inMinimizationRoot() {
+ val desk = createDesk()
+
+ assertThat(
+ organizer.isDeskChange(
+ change =
+ TransitionInfo.Change(
+ desk.minimizationRoot.token,
+ desk.minimizationRoot.leash,
+ )
+ .apply { taskInfo = desk.minimizationRoot.taskInfo },
+ deskId = desk.deskRoot.deskId,
+ )
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun isDeskChange_anyDesk() {
+ val desk = createDesk()
+
+ assertThat(
+ organizer.isDeskChange(
+ change =
+ TransitionInfo.Change(desk.deskRoot.taskInfo.token, desk.deskRoot.leash)
+ .apply { taskInfo = desk.deskRoot.taskInfo }
+ )
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun isDeskChange_anyDesk_inMinimizationRoot() {
+ val desk = createDesk()
+
+ assertThat(
+ organizer.isDeskChange(
+ change =
+ TransitionInfo.Change(
+ desk.minimizationRoot.taskInfo.token,
+ desk.minimizationRoot.leash,
+ )
+ .apply { taskInfo = desk.minimizationRoot.taskInfo }
)
)
.isTrue()
}
- private fun createDesk(): DeskRoot {
+ @Test
+ fun minimizeTask() {
+ val desk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
+ val wct = WindowContainerTransaction()
+ organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task)
+ organizer.onTaskAppeared(task, SurfaceControl())
+
+ organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+
+ assertThat(
+ wct.hierarchyOps.any { hop ->
+ hop.isReparent &&
+ hop.container == task.token.asBinder() &&
+ hop.newParent == desk.minimizationRoot.token.asBinder()
+ }
+ )
+ .isTrue()
+ }
+
+ @Test
+ fun minimizeTask_alreadyMinimized_noOp() {
+ val desk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
+ val wct = WindowContainerTransaction()
+ organizer.onTaskAppeared(task, SurfaceControl())
+
+ organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+
+ assertThat(wct.isEmpty).isTrue()
+ }
+
+ @Test
+ fun minimizeTask_inDifferentDesk_noOp() {
+ val desk = createDesk()
+ val otherDesk = createDesk()
+ val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId }
+ val wct = WindowContainerTransaction()
+ organizer.onTaskAppeared(task, SurfaceControl())
+
+ organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task)
+
+ assertThat(wct.isEmpty).isTrue()
+ }
+
+ private data class DeskRoots(
+ val deskRoot: DeskRoot,
+ val minimizationRoot: DeskMinimizationRoot,
+ )
+
+ private fun createDesk(): DeskRoots {
organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- return organizer.roots[freeformRoot.taskId]
+ val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 }
+ organizer.onTaskAppeared(minimizationRoot, SurfaceControl())
+ return DeskRoots(
+ organizer.deskRootsByDeskId[freeformRoot.taskId],
+ checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]),
+ )
}
private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
index fd5e567f69ed..93dd3456f3f2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java
@@ -17,16 +17,20 @@
package com.android.wm.shell.recents;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX;
+import static com.android.wm.shell.Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION;
import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING;
import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING;
import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION;
import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION;
import static com.google.common.truth.Truth.assertThat;
@@ -64,7 +68,6 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.os.IResultReceiver;
-import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
@@ -73,6 +76,7 @@ import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.shared.R;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -308,8 +312,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
mRecentsTransitionHandler.findController(transition).merge(
mergeTransitionInfo,
new StubTransaction(),
- finishT,
- transition,
+ new StubTransaction(),
mock(Transitions.TransitionFinishCallback.class));
mMainExecutor.flushAll();
@@ -318,6 +321,69 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
}
@Test
+ @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION)
+ public void testMerge_consumeBookendTransition() throws Exception {
+ // Start and finish the transition
+ final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class);
+ final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner);
+ mRecentsTransitionHandler.startAnimation(
+ transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
+ false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+ mMainExecutor.flushAll();
+
+ // Merge the bookend transition
+ TransitionInfo mergeTransitionInfo =
+ new TransitionInfoBuilder(TRANSIT_END_RECENTS_TRANSITION)
+ .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build())
+ .build();
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ Transitions.TransitionFinishCallback finishCallback
+ = mock(Transitions.TransitionFinishCallback.class);
+ mRecentsTransitionHandler.findController(transition).merge(
+ mergeTransitionInfo,
+ new StubTransaction(),
+ finishT,
+ finishCallback);
+ mMainExecutor.flushAll();
+
+ // Verify that we've merged
+ verify(finishCallback).onTransitionFinished(any());
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION)
+ public void testMerge_pendingBookendTransition_mergesTransition() throws Exception {
+ // Start and finish the transition
+ final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class);
+ final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner);
+ mRecentsTransitionHandler.startAnimation(
+ transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+ mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
+ false /* sendUserLeaveHint */, mock(IResultReceiver.class));
+ mMainExecutor.flushAll();
+
+ // Merge a new transition while we have a pending finish
+ TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build())
+ .build();
+ SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+ Transitions.TransitionFinishCallback finishCallback
+ = mock(Transitions.TransitionFinishCallback.class);
+ mRecentsTransitionHandler.findController(transition).merge(
+ mergeTransitionInfo,
+ new StubTransaction(),
+ finishT,
+ finishCallback);
+ mMainExecutor.flushAll();
+
+ // Verify that we've cleaned up the original transition
+ assertNull(mRecentsTransitionHandler.findController(transition));
+ }
+
+ @Test
@EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX)
public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() {
ActivityManager.RunningTaskInfo freeformTask =
@@ -336,7 +402,6 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
mergeTransitionInfo,
new StubTransaction(),
finishT,
- transition,
mock(Transitions.TransitionFinishCallback.class));
mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false,
false /* sendUserLeaveHint */, mock(IResultReceiver.class));
@@ -385,15 +450,23 @@ public class RecentsTransitionHandlerTest extends ShellTestCase {
}
private TransitionInfo createTransitionInfo() {
- final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder()
+ final ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder()
.setTopActivityType(ACTIVITY_TYPE_HOME)
.build();
+ final ActivityManager.RunningTaskInfo appTask = new TestRunningTaskInfoBuilder()
+ .setTopActivityType(ACTIVITY_TYPE_STANDARD)
+ .build();
final TransitionInfo.Change homeChange = new TransitionInfo.Change(
- task.token, new SurfaceControl());
+ homeTask.token, new SurfaceControl());
homeChange.setMode(TRANSIT_TO_FRONT);
- homeChange.setTaskInfo(task);
+ homeChange.setTaskInfo(homeTask);
+ final TransitionInfo.Change appChange = new TransitionInfo.Change(
+ appTask.token, new SurfaceControl());
+ appChange.setMode(TRANSIT_TO_FRONT);
+ appChange.setTaskInfo(appTask);
return new TransitionInfoBuilder(TRANSIT_START_RECENTS_TRANSITION)
.addChange(homeChange)
+ .addChange(appChange)
.build();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
index f69bf34ea3f7..88c6e499b869 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
@@ -16,13 +16,16 @@
package com.android.wm.shell.shared.desktopmode
+import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.app.TaskInfo
import android.compat.testing.PlatformCompatChangeRule
import android.content.ComponentName
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Process
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
@@ -39,7 +42,9 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -55,6 +60,7 @@ class DesktopModeCompatPolicyTest : ShellTestCase() {
private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
private val packageManager: PackageManager = mock()
private val homeActivities = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
+ private val baseActivityTest = ComponentName("com.test.dummypackage", "TestClass")
@Before
fun setUp() {
@@ -64,6 +70,7 @@ class DesktopModeCompatPolicyTest : ShellTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun testIsTopActivityExemptFromDesktopWindowing_onlyTransparentActivitiesInStack() {
assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
@@ -71,10 +78,39 @@ class DesktopModeCompatPolicyTest : ShellTestCase() {
isActivityStackTransparent = true
isTopActivityNoDisplay = false
numActivities = 1
+ baseActivity = baseActivityTest
}))
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
+ fun testIsTopActivityExemptWithPermission_onlyTransparentActivitiesInStack() {
+ allowOverlayPermission(arrayOf(SYSTEM_ALERT_WINDOW))
+ assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
+ createFreeformTask(/* displayId */ 0)
+ .apply {
+ isActivityStackTransparent = true
+ isTopActivityNoDisplay = false
+ numActivities = 1
+ baseActivity = baseActivityTest
+ }))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
+ fun testIsTopActivityExemptWithNoPermission_onlyTransparentActivitiesInStack() {
+ allowOverlayPermission(arrayOf())
+ assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
+ createFreeformTask(/* displayId */ 0)
+ .apply {
+ isActivityStackTransparent = true
+ isTopActivityNoDisplay = false
+ numActivities = 1
+ baseActivity = baseActivityTest
+ }))
+ }
+
+ @Test
fun testIsTopActivityExemptFromDesktopWindowing_noActivitiesInStack() {
assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(
createFreeformTask(/* displayId */ 0)
@@ -219,4 +255,15 @@ class DesktopModeCompatPolicyTest : ShellTestCase() {
}
}
}
+
+ fun allowOverlayPermission(permissions: Array<String>) {
+ val packageInfo = mock<PackageInfo>()
+ packageInfo.requestedPermissions = permissions
+ whenever(
+ packageManager.getPackageInfo(
+ anyString(),
+ eq(PackageManager.GET_PERMISSIONS)
+ )
+ ).thenReturn(packageInfo)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 6f28e656d060..3099b0f5cf66 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -17,36 +17,44 @@
package com.android.wm.shell.transition;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.WindowConfiguration.ActivityType;
import android.content.Context;
+import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.TransitionInfo.TransitionMode;
+import android.window.WindowContainerToken;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
@@ -188,6 +196,72 @@ public class HomeTransitionObserverTest extends ShellTestCase {
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE})
+ public void testDragTaskToBubbleOverHome_notifiesHomeIsVisible() throws RemoteException {
+ ActivityManager.RunningTaskInfo homeTask = createTaskInfo(1, ACTIVITY_TYPE_HOME);
+ ActivityManager.RunningTaskInfo bubbleTask = createTaskInfo(2, ACTIVITY_TYPE_STANDARD);
+
+ TransitionInfo startDragTransition =
+ new TransitionInfoBuilder(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP)
+ .addChange(TRANSIT_TO_FRONT, homeTask)
+ .addChange(TRANSIT_TO_BACK, bubbleTask)
+ .build();
+
+ // Start drag to desktop which brings home to front
+ mHomeTransitionObserver.onTransitionReady(new Binder(), startDragTransition,
+ MockTransactionPool.create(), MockTransactionPool.create());
+ // Does not notify home visibility yet
+ verify(mListener, never()).onHomeVisibilityChanged(anyBoolean());
+
+ TransitionInfo convertToBubbleTransition =
+ new TransitionInfoBuilder(TRANSIT_CONVERT_TO_BUBBLE)
+ .addChange(TRANSIT_TO_FRONT, bubbleTask)
+ .build();
+
+ // Convert to bubble. Transition does not include changes for home task
+ mHomeTransitionObserver.onTransitionReady(new Binder(), convertToBubbleTransition,
+ MockTransactionPool.create(), MockTransactionPool.create());
+
+ // Notifies home visibility change that was pending from the start of drag
+ verify(mListener).onHomeVisibilityChanged(true);
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE})
+ public void testDragTaskToBubbleOverOtherTask_notifiesHomeIsNotVisible()
+ throws RemoteException {
+ ActivityManager.RunningTaskInfo homeTask = createTaskInfo(1, ACTIVITY_TYPE_HOME);
+ ActivityManager.RunningTaskInfo bubbleTask = createTaskInfo(2, ACTIVITY_TYPE_STANDARD);
+ ActivityManager.RunningTaskInfo otherTask = createTaskInfo(3, ACTIVITY_TYPE_STANDARD);
+
+ TransitionInfo startDragTransition =
+ new TransitionInfoBuilder(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP)
+ .addChange(TRANSIT_TO_FRONT, homeTask)
+ .addChange(TRANSIT_TO_BACK, bubbleTask)
+ .build();
+
+ // Start drag to desktop which brings home to front
+ mHomeTransitionObserver.onTransitionReady(new Binder(), startDragTransition,
+ MockTransactionPool.create(), MockTransactionPool.create());
+ // Does not notify home visibility yet
+ verify(mListener, never()).onHomeVisibilityChanged(anyBoolean());
+
+ TransitionInfo convertToBubbleTransition =
+ new TransitionInfoBuilder(TRANSIT_CONVERT_TO_BUBBLE)
+ .addChange(TRANSIT_TO_FRONT, bubbleTask)
+ .addChange(TRANSIT_TO_FRONT, otherTask)
+ .addChange(TRANSIT_TO_BACK, homeTask)
+ .build();
+
+ // Convert to bubble. Transition includes home task to back which updates home visibility
+ mHomeTransitionObserver.onTransitionReady(new Binder(), convertToBubbleTransition,
+ MockTransactionPool.create(), MockTransactionPool.create());
+
+ // Notifies home visibility change due to home moving to back in the second transition
+ verify(mListener).onHomeVisibilityChanged(false);
+ }
+
+ @Test
public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose()
throws RemoteException {
TransitionInfo info = mock(TransitionInfo.class);
@@ -227,4 +301,14 @@ public class HomeTransitionObserverTest extends ShellTestCase {
when(change.getMode()).thenReturn(mode);
taskInfo.isRunning = isRunning;
}
+
+ private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int activityType) {
+ ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
+ taskInfo.taskId = taskId;
+ taskInfo.topActivityType = activityType;
+ taskInfo.configuration.windowConfiguration.setActivityType(activityType);
+ taskInfo.token = mock(WindowContainerToken.class);
+ taskInfo.isRunning = true;
+ return taskInfo;
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index 8cccdb2b6120..81dfaed56b6f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -52,6 +52,7 @@ import com.android.wm.shell.common.DisplayChangeController
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler
@@ -138,6 +139,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
protected val mockFreeformTaskTransitionStarter = mock<FreeformTaskTransitionStarter>()
protected val mockActivityOrientationChangeHandler =
mock<DesktopActivityOrientationChangeHandler>()
+ protected val mockMultiDisplayDragMoveIndicatorController =
+ mock<MultiDisplayDragMoveIndicatorController>()
protected val mockInputManager = mock<InputManager>()
private val mockTaskPositionerFactory =
mock<DesktopModeWindowDecorViewModel.TaskPositionerFactory>()
@@ -229,6 +232,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
mockRecentsTransitionHandler,
desktopModeCompatPolicy,
mockTilingWindowDecoration,
+ mockMultiDisplayDragMoveIndicatorController,
)
desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -243,6 +247,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
any(),
any(),
any(),
+ any(),
any()
)
)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt
index 937938df82c8..a6b077037b86 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt
@@ -41,6 +41,7 @@ import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController
import com.android.wm.shell.common.MultiDisplayTestUtil
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
@@ -62,8 +63,8 @@ import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.Mockito.`when`
+import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
/**
@@ -93,7 +94,8 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() {
@Mock private lateinit var mockTransitions: Transitions
@Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor
@Mock private lateinit var mockSurfaceControl: SurfaceControl
-
+ @Mock private lateinit var mockMultiDisplayDragMoveIndicatorController:
+ MultiDisplayDragMoveIndicatorController
private lateinit var resources: TestableResources
private lateinit var spyDisplayLayout0: DisplayLayout
private lateinit var spyDisplayLayout1: DisplayLayout
@@ -170,10 +172,11 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() {
mockDesktopWindowDecoration,
mockDisplayController,
mockDragEventListener,
- mockTransactionFactory,
+ { mockTransaction },
mockTransitions,
mockInteractionJankMonitor,
mainHandler,
+ mockMultiDisplayDragMoveIndicatorController,
)
}
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index f5e10d94452f..7a51c20f7672 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -44,6 +44,9 @@ namespace android {
namespace {
+constexpr int32_t kDefaultDisplayId = 0;
+constexpr int32_t kDefaultDeviceId = 0;
+
using EntryValue = std::variant<Res_value, incfs::verified_map_ptr<ResTable_map_entry>>;
/* NOTE: table_entry has been verified in LoadedPackage::GetEntryFromOffset(),
@@ -61,7 +64,7 @@ base::expected<EntryValue, IOError> GetEntryValue(
return table_entry->value();
}
-} // namespace
+} // namespace
struct FindEntryResult {
// The cookie representing the ApkAssets in which the value resides.
@@ -99,14 +102,15 @@ struct Theme::Entry {
Res_value value;
};
-AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration) {
+AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration)
+ : display_id_(kDefaultDisplayId), device_id_(kDefaultDeviceId) {
configurations_.push_back(configuration);
// Don't invalidate caches here as there's nothing cached yet.
SetApkAssets(apk_assets, false);
}
-AssetManager2::AssetManager2() {
+AssetManager2::AssetManager2() : display_id_(kDefaultDisplayId), device_id_(kDefaultDeviceId) {
configurations_.emplace_back();
}
@@ -172,8 +176,7 @@ void AssetManager2::BuildDynamicRefTable(ApkAssetsList apk_assets) {
// to take effect.
auto iter = target_assets_package_ids.find(loaded_idmap->TargetApkPath());
if (iter == target_assets_package_ids.end()) {
- LOG(INFO) << "failed to find target package for overlay "
- << loaded_idmap->OverlayApkPath();
+ LOG(INFO) << "failed to find target package for overlay " << loaded_idmap->OverlayApkPath();
} else {
uint8_t target_package_id = iter->second;
@@ -189,10 +192,11 @@ void AssetManager2::BuildDynamicRefTable(ApkAssetsList apk_assets) {
<< " assigned package group";
PackageGroup& target_package_group = package_groups_[target_idx];
- target_package_group.overlays_.push_back(
- ConfiguredOverlay{loaded_idmap->GetTargetResourcesMap(target_package_id,
- overlay_ref_table.get()),
- apk_assets_cookies[apk_assets]});
+ target_package_group.overlays_.push_back(ConfiguredOverlay{
+ loaded_idmap->GetTargetResourcesMap(target_package_id, overlay_ref_table.get()),
+ apk_assets_cookies[apk_assets],
+ IsAnyOverlayConstraintSatisfied(loaded_idmap->GetConstraints())
+ });
}
}
@@ -291,7 +295,7 @@ void AssetManager2::DumpToLog() const {
}
LOG(INFO) << "Package ID map: " << list;
- for (const auto& package_group: package_groups_) {
+ for (const auto& package_group : package_groups_) {
list = "";
for (const auto& package : package_group.packages_) {
const LoadedPackage* loaded_package = package.loaded_package_;
@@ -347,7 +351,6 @@ std::shared_ptr<const DynamicRefTable> AssetManager2::GetDynamicRefTableForCooki
const std::unordered_map<std::string, std::string>*
AssetManager2::GetOverlayableMapForPackage(uint32_t package_id) const {
-
if (package_id >= package_ids_.size()) {
return nullptr;
}
@@ -462,6 +465,28 @@ void AssetManager2::SetConfigurations(std::span<const ResTable_config> configura
}
}
+void AssetManager2::SetOverlayConstraints(int32_t display_id, int32_t device_id) {
+ bool changed = false;
+ if (display_id_ != display_id) {
+ display_id_ = display_id;
+ changed = true;
+ }
+ if (device_id_ != device_id) {
+ device_id_ = device_id;
+ changed = true;
+ }
+ if (changed) {
+ // Enable/disable overlays based on current constraints
+ for (PackageGroup& group : package_groups_) {
+ for (auto &overlay: group.overlays_) {
+ overlay.enabled = IsAnyOverlayConstraintSatisfied(
+ overlay.overlay_res_maps_.GetConstraints());
+ }
+ }
+ InvalidateCaches(static_cast<uint32_t>(-1));
+ }
+}
+
std::set<AssetManager2::ApkAssetsPtr> AssetManager2::GetNonSystemOverlays() const {
std::set<ApkAssetsPtr> non_system_overlays;
for (const PackageGroup& package_group : package_groups_) {
@@ -475,6 +500,8 @@ std::set<AssetManager2::ApkAssetsPtr> AssetManager2::GetNonSystemOverlays() cons
if (!found_system_package) {
auto op = StartOperation();
+ // Return all overlays, including the disabled ones as this is used for static info
+ // collection only.
for (const ConfiguredOverlay& overlay : package_group.overlays_) {
if (const auto& asset = GetApkAssets(overlay.cookie)) {
non_system_overlays.insert(std::move(asset));
@@ -651,7 +678,6 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
auto op = StartOperation();
-
// Retrieve the package group from the package id of the resource id.
if (UNLIKELY(!is_valid_resid(resid))) {
LOG(ERROR) << base::StringPrintf("Invalid resource ID 0x%08x.", resid);
@@ -672,7 +698,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
std::optional<FindEntryResult> final_result;
bool final_has_locale = false;
bool final_overlaid = false;
- for (auto & config : configurations_) {
+ for (auto& config : configurations_) {
// Might use this if density_override != 0.
ResTable_config density_override_config;
@@ -698,7 +724,8 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
}
if (!assets->IsLoader()) {
for (const auto& id_map : package_group.overlays_) {
- auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid);
+ auto overlay_entry = id_map.enabled ?
+ id_map.overlay_res_maps_.Lookup(resid) : IdmapResMap::Result();
if (!overlay_entry) {
// No id map entry exists for this target resource.
continue;
@@ -708,7 +735,7 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
ConfigDescription best_frro_config;
Res_value best_frro_value;
bool frro_found = false;
- for( const auto& [config, value] : overlay_entry.GetInlineValue()) {
+ for (const auto& [config, value] : overlay_entry.GetInlineValue()) {
if ((!frro_found || config.isBetterThan(best_frro_config, desired_config))
&& config.match(*desired_config)) {
frro_found = true;
@@ -1011,7 +1038,7 @@ std::string AssetManager2::GetLastResourceResolution() const {
resid, resource_name_string.c_str(), conf.toString().c_str());
char str[40];
str[0] = '\0';
- for(auto iter = configurations_.begin(); iter < configurations_.end(); iter++) {
+ for (auto iter = configurations_.begin(); iter < configurations_.end(); iter++) {
iter->getBcp47Locale(str);
log_stream << base::StringPrintf(" %s%s", str, iter < configurations_.end() ? "," : "");
}
@@ -1504,7 +1531,7 @@ void AssetManager2::RebuildFilterList() {
package.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) {
FilteredConfigGroup* group = nullptr;
for (const auto& type_entry : type_spec.type_entries) {
- for (auto & config : configurations_) {
+ for (auto& config : configurations_) {
if (type_entry.config.match(config)) {
if (!group) {
group = &package.filtered_configs_.editItemAt(type_id - 1);
@@ -1521,6 +1548,27 @@ void AssetManager2::RebuildFilterList() {
}
}
+bool AssetManager2::IsAnyOverlayConstraintSatisfied(const Idmap_constraints& constraints) const {
+ if (constraints.constraint_count == 0) {
+ // There are no constraints, return true.
+ return true;
+ }
+
+ for (uint32_t i = 0; i < constraints.constraint_count; i++) {
+ auto constraint = constraints.constraint_entries[i];
+ if (constraint.constraint_type == kOverlayConstraintTypeDisplayId &&
+ constraint.constraint_value == display_id_) {
+ return true;
+ }
+ if (constraint.constraint_type == kOverlayConstraintTypeDeviceId &&
+ constraint.constraint_value == device_id_) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
void AssetManager2::InvalidateCaches(uint32_t diff) {
cached_resolved_values_.clear();
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index f0ef97e5bdcc..8d1de1af56d2 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -56,13 +56,6 @@ struct Idmap_header {
// without having to read/store each header entry separately.
};
-struct Idmap_constraint {
- // Constraint type can be TYPE_DISPLAY_ID or TYP_DEVICE_ID, please refer
- // to ConstraintType in OverlayConstraint.java
- uint32_t constraint_type;
- uint32_t constraint_value;
-};
-
struct Idmap_data_header {
uint32_t target_entry_count;
uint32_t target_inline_entry_count;
@@ -148,12 +141,13 @@ status_t OverlayDynamicRefTable::lookupResourceIdNoRewrite(uint32_t* resId) cons
return DynamicRefTable::lookupResourceId(resId);
}
-IdmapResMap::IdmapResMap(const Idmap_data_header* data_header, Idmap_target_entries entries,
- Idmap_target_inline_entries inline_entries,
+IdmapResMap::IdmapResMap(const Idmap_data_header* data_header, const Idmap_constraints& constraints,
+ Idmap_target_entries entries, Idmap_target_inline_entries inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
const ConfigDescription* configs, uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table)
: data_header_(data_header),
+ constraints_(constraints),
entries_(entries),
inline_entries_(inline_entries),
inline_entry_values_(inline_entry_values),
@@ -254,7 +248,7 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size
}
return std::string_view(data, *len);
}
-} // namespace
+} // namespace
// O_PATH is a lightweight way of creating an FD, only exists on Linux
#ifndef O_PATH
@@ -262,9 +256,7 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size
#endif
LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* header,
- const Idmap_constraint* constraints,
- uint32_t constraints_count,
- const Idmap_data_header* data_header,
+ const Idmap_data_header* data_header, const Idmap_constraints& constraints,
Idmap_target_entries target_entries,
Idmap_target_inline_entries target_inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
@@ -272,9 +264,8 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head
std::unique_ptr<ResStringPool>&& string_pool,
std::string_view overlay_apk_path, std::string_view target_apk_path)
: header_(header),
- constraints_(constraints),
- constraints_count_(constraints_count),
data_header_(data_header),
+ constraints_(constraints),
target_entries_(target_entries),
target_inline_entries_(target_inline_entries),
inline_entry_values_(inline_entry_values),
@@ -328,16 +319,20 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
return {};
}
- auto constraints_count = ReadType<uint32_t>(&data_ptr, &data_size, "constraints count");
- if (!constraints_count) {
+ auto constraint_count = ReadType<uint32_t>(&data_ptr, &data_size, "constraint count");
+ if (!constraint_count) {
+ LOG(ERROR) << "idmap doesn't have constraint count";
return {};
}
- auto constraints = *constraints_count > 0 ?
- ReadType<Idmap_constraint>(&data_ptr, &data_size, "constraints", *constraints_count)
+ auto constraint_entries = *constraint_count > 0 ?
+ ReadType<Idmap_constraint>(&data_ptr, &data_size, "constraints", dtohl(*constraint_count))
: nullptr;
- if (*constraints_count > 0 && !constraints) {
+ if (*constraint_count > 0 && !constraint_entries) {
+ LOG(ERROR) << "no constraint entries in idmap with non-zero constraints";
return {};
}
+ Idmap_constraints constraints{.constraint_count = *constraint_count,
+ .constraint_entries = constraint_entries};
// Parse the idmap data blocks. Currently idmap2 can only generate one data block.
auto data_header = ReadType<Idmap_data_header>(&data_ptr, &data_size, "data header");
@@ -405,10 +400,9 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
// Can't use make_unique because LoadedIdmap constructor is private.
return std::unique_ptr<LoadedIdmap>(
- new LoadedIdmap(std::string(idmap_path), header, constraints, *constraints_count,
- data_header, target_entries, target_inline_entries,
- target_inline_entry_values,configurations, overlay_entries,
- std::move(idmap_string_pool),*overlay_path, *target_path));
+ new LoadedIdmap(std::string(idmap_path), header, data_header, constraints, target_entries,
+ target_inline_entries, target_inline_entry_values, configurations,
+ overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path));
}
UpToDate LoadedIdmap::IsUpToDate() const {
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index 0fdeefa09e26..a47fe6a7f50d 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -171,6 +171,8 @@ class AssetManager2 {
default_locale_ = default_locale;
}
+ void SetOverlayConstraints(int32_t display_id, int32_t device_id);
+
// Returns all configurations for which there are resources defined, or an I/O error if reading
// resource data failed.
//
@@ -389,6 +391,9 @@ class AssetManager2 {
// The cookie of the overlay assets.
ApkAssetsCookie cookie;
+
+ // Enable/disable status of the overlay based on current constraints of AssetManager.
+ bool enabled;
};
// Represents a logical package, which can be made up of many individual packages. Each package
@@ -457,6 +462,8 @@ class AssetManager2 {
// promoted apk assets when the last operation ends.
void FinishOperation() const;
+ bool IsAnyOverlayConstraintSatisfied(const Idmap_constraints& constraints) const;
+
// The ordered list of ApkAssets to search. These are not owned by the AssetManager, and must
// have a longer lifetime.
// The second pair element is the promoted version of the assets, that is held for the duration
@@ -480,6 +487,9 @@ class AssetManager2 {
// may need to be purged.
ftl::SmallVector<ResTable_config, 1> configurations_;
+ int32_t display_id_;
+ int32_t device_id_;
+
// Cached set of bags. These are cached because they can inherit keys from parent bags,
// which involves some calculation.
mutable std::unordered_map<uint32_t, util::unique_cptr<ResolvedBag>> cached_bags_;
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 0c0856315d8f..939b62462560 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -59,13 +59,25 @@ inline UpToDate fromBool(bool value) {
class LoadedIdmap;
class IdmapResMap;
struct Idmap_header;
-struct Idmap_constraint;
+struct Idmap_constraints;
struct Idmap_data_header;
-struct Idmap_target_entry;
struct Idmap_target_entry_inline;
struct Idmap_target_entry_inline_value;
-struct Idmap_overlay_entry;
+// LINT.IfChange
+constexpr int32_t kOverlayConstraintTypeDisplayId = 0;
+constexpr int32_t kOverlayConstraintTypeDeviceId = 1;
+// LINT.ThenChange(../../../../core/java/android/content/om/OverlayConstraint.java)
+
+struct Idmap_constraint {
+ // Constraint type can be kOverlayConstraintTypeDisplayId or kOverlayConstraintTypeDeviceId
+ const uint32_t constraint_type;
+ const uint32_t constraint_value;
+};
+struct Idmap_constraints {
+ const uint32_t constraint_count = 0;
+ const Idmap_constraint* constraint_entries = nullptr;
+};
struct Idmap_target_entries {
const uint32_t* target_id = nullptr;
const uint32_t* overlay_id = nullptr;
@@ -169,14 +181,19 @@ class IdmapResMap {
return overlay_ref_table_;
}
+ inline Idmap_constraints GetConstraints() const {
+ return constraints_;
+ }
+
private:
- explicit IdmapResMap(const Idmap_data_header* data_header, Idmap_target_entries entries,
- Idmap_target_inline_entries inline_entries,
+ explicit IdmapResMap(const Idmap_data_header* data_header, const Idmap_constraints& constraints,
+ Idmap_target_entries entries, Idmap_target_inline_entries inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
const ConfigDescription* configs, uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table);
const Idmap_data_header* data_header_;
+ Idmap_constraints constraints_;
Idmap_target_entries entries_;
Idmap_target_inline_entries inline_entries_;
const Idmap_target_entry_inline_value* inline_entry_values_;
@@ -210,8 +227,9 @@ class LoadedIdmap {
// Returns a mapping from target resource ids to overlay values.
IdmapResMap GetTargetResourcesMap(uint8_t target_assigned_package_id,
const OverlayDynamicRefTable* overlay_ref_table) const {
- return IdmapResMap(data_header_, target_entries_, target_inline_entries_, inline_entry_values_,
- configurations_, target_assigned_package_id, overlay_ref_table);
+ return IdmapResMap(data_header_, constraints_, target_entries_, target_inline_entries_,
+ inline_entry_values_, configurations_, target_assigned_package_id,
+ overlay_ref_table);
}
// Returns a dynamic reference table for a loaded overlay package.
@@ -223,14 +241,17 @@ class LoadedIdmap {
// LoadedIdmap.
UpToDate IsUpToDate() const;
+ inline const Idmap_constraints GetConstraints() const {
+ return constraints_;
+ }
+
protected:
// Exposed as protected so that tests can subclass and mock this class out.
LoadedIdmap() = default;
const Idmap_header* header_;
- const Idmap_constraint* constraints_;
- uint32_t constraints_count_;
const Idmap_data_header* data_header_;
+ Idmap_constraints constraints_;
Idmap_target_entries target_entries_;
Idmap_target_inline_entries target_inline_entries_;
const Idmap_target_entry_inline_value* inline_entry_values_;
@@ -247,9 +268,7 @@ class LoadedIdmap {
DISALLOW_COPY_AND_ASSIGN(LoadedIdmap);
explicit LoadedIdmap(const std::string& idmap_path, const Idmap_header* header,
- const Idmap_constraint* constraints,
- uint32_t constraints_count,
- const Idmap_data_header* data_header,
+ const Idmap_data_header* data_header, const Idmap_constraints& constraints,
Idmap_target_entries target_entries,
Idmap_target_inline_entries target_inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values_,
diff --git a/libs/hostgraphics/include/gui/BufferItemConsumer.h b/libs/hostgraphics/include/gui/BufferItemConsumer.h
index c25941151800..5c96c82e061c 100644
--- a/libs/hostgraphics/include/gui/BufferItemConsumer.h
+++ b/libs/hostgraphics/include/gui/BufferItemConsumer.h
@@ -17,6 +17,8 @@
#ifndef ANDROID_GUI_BUFFERITEMCONSUMER_H
#define ANDROID_GUI_BUFFERITEMCONSUMER_H
+#include <com_android_graphics_libgui_flags.h>
+#include <gui/BufferQueue.h>
#include <gui/ConsumerBase.h>
#include <gui/IGraphicBufferConsumer.h>
#include <utils/RefBase.h>
@@ -26,9 +28,22 @@ namespace android {
class BufferItemConsumer : public ConsumerBase {
public:
BufferItemConsumer(const sp<IGraphicBufferConsumer>& consumer, uint64_t consumerUsage,
- int bufferCount, bool controlledByApp)
+ int bufferCount = -1, bool controlledByApp = false)
: mConsumer(consumer) {}
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+ BufferItemConsumer(uint64_t consumerUsage, int bufferCount = -1,
+ bool controlledByApp = false, bool isConsumerSurfaceFlinger = false) {
+ sp<IGraphicBufferProducer> producer;
+ BufferQueue::createBufferQueue(&producer, &mConsumer);
+ mSurface = sp<Surface>::make(producer, controlledByApp);
+ }
+
+ status_t setConsumerIsProtected(bool isProtected) {
+ return OK;
+ }
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+
status_t acquireBuffer(BufferItem* item, nsecs_t presentWhen, bool waitForFence = true) {
return mConsumer->acquireBuffer(item, presentWhen, 0);
}
@@ -71,8 +86,20 @@ public:
return OK;
}
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+// Returns a Surface that can be used as the producer for this consumer.
+ sp<Surface> getSurface() const {
+ return mSurface;
+ }
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+
private:
sp<IGraphicBufferConsumer> mConsumer;
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+ // This Surface wraps the IGraphicBufferConsumer created for this
+ // ConsumerBase.
+ sp<Surface> mSurface;
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
};
} // namespace android
diff --git a/libs/hostgraphics/include/gui/Surface.h b/libs/hostgraphics/include/gui/Surface.h
index 2774f89cb54c..e268ce6ca769 100644
--- a/libs/hostgraphics/include/gui/Surface.h
+++ b/libs/hostgraphics/include/gui/Surface.h
@@ -34,6 +34,10 @@ public:
ANativeWindow::query = hook_query;
}
+ sp<IGraphicBufferProducer> getIGraphicBufferProducer() const {
+ return mBufferProducer;
+ }
+
static bool isValid(const sp<Surface>& surface) {
return surface != nullptr;
}
diff --git a/libs/hostgraphics/include/ui/Fence.h b/libs/hostgraphics/include/ui/Fence.h
index 187c3116f61c..3364b8aed605 100644
--- a/libs/hostgraphics/include/ui/Fence.h
+++ b/libs/hostgraphics/include/ui/Fence.h
@@ -60,6 +60,10 @@ public:
return 0;
}
+ int get() const {
+ return 0;
+ }
+
inline Status getStatus() {
// The sync_wait call underlying wait() has been measured to be
// significantly faster than the sync_fence_info call underlying
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index bb2a53bc04d6..38ac8ab7135e 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -233,6 +233,14 @@ java_sdk_library {
}
filegroup {
+ name: "framework-graphics-ravenwood-policies",
+ srcs: [
+ "framework-graphics-ravenwood-policies.txt",
+ ],
+ visibility: ["//frameworks/base/ravenwood"],
+}
+
+filegroup {
name: "framework-graphics-srcs",
srcs: [
"apex/java/**/*.java",
@@ -461,6 +469,10 @@ cc_defaults {
},
linux: {
srcs: ["platform/linux/utils/SharedLib.cpp"],
+ shared_libs: [
+ "libbinder",
+ "libbinder_ndk",
+ ],
},
darwin: {
srcs: ["platform/darwin/utils/SharedLib.cpp"],
diff --git a/libs/hwui/framework-graphics-ravenwood-policies.txt b/libs/hwui/framework-graphics-ravenwood-policies.txt
new file mode 100644
index 000000000000..7296225ccfe8
--- /dev/null
+++ b/libs/hwui/framework-graphics-ravenwood-policies.txt
@@ -0,0 +1 @@
+class android.graphics.ColorMatrix keepclass
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index cfde0b28c0d5..27d4ac7cef4b 100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -613,7 +613,7 @@ static void Bitmap_setHasMipMap(JNIEnv* env, jobject, jlong bitmapHandle,
///////////////////////////////////////////////////////////////////////////////
// TODO: Move somewhere else
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
#define ON_ERROR_RETURN(X) \
if ((error = (X)) != STATUS_OK) return error
@@ -717,7 +717,7 @@ static binder_status_t writeBlob(AParcel* parcel, uint64_t bitmapId, const SkBit
#undef ON_ERROR_RETURN
-#endif // __ANDROID__ // Layoutlib does not support parcel
+#endif // __linux__ // Only Linux support parcel
// This is the maximum possible size because the SkColorSpace must be
// representable (and therefore serializable) using a matrix and numerical
@@ -733,7 +733,7 @@ static bool validateImageInfo(const SkImageInfo& info, int32_t rowBytes) {
}
static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
if (parcel == NULL) {
jniThrowNullPointerException(env, "parcel cannot be null");
return NULL;
@@ -836,14 +836,14 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr,
nullptr, density, sourceId);
#else
- jniThrowRuntimeException(env, "Cannot use parcels outside of Android");
+ jniThrowRuntimeException(env, "Cannot use parcels outside of Linux");
return NULL;
#endif
}
static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density,
jobject parcel) {
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
if (parcel == NULL) {
ALOGD("------- writeToParcel null parcel\n");
return JNI_FALSE;
@@ -901,7 +901,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, j
}
return JNI_TRUE;
#else
- doThrowRE(env, "Cannot use parcels outside of Android");
+ doThrowRE(env, "Cannot use parcels outside of Linux");
return JNI_FALSE;
#endif
}
diff --git a/libs/hwui/jni/Region.cpp b/libs/hwui/jni/Region.cpp
index 1e064b820591..76986eeb079d 100644
--- a/libs/hwui/jni/Region.cpp
+++ b/libs/hwui/jni/Region.cpp
@@ -18,7 +18,7 @@
#include "SkPath.h"
#include "GraphicsJNI.h"
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
#include <android/binder_parcel.h>
#include <android/binder_parcel_jni.h>
#include <android/binder_parcel_utils.h>
@@ -202,7 +202,7 @@ static jstring Region_toString(JNIEnv* env, jobject clazz, jlong regionHandle) {
static jlong Region_createFromParcel(JNIEnv* env, jobject clazz, jobject parcel)
{
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
if (parcel == nullptr) {
return 0;
}
@@ -230,7 +230,7 @@ static jlong Region_createFromParcel(JNIEnv* env, jobject clazz, jobject parcel)
static jboolean Region_writeToParcel(JNIEnv* env, jobject clazz, jlong regionHandle, jobject parcel)
{
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
const SkRegion* region = reinterpret_cast<SkRegion*>(regionHandle);
if (parcel == nullptr) {
return JNI_FALSE;
diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp
index 95e4e01d8df8..52cd988344b0 100644
--- a/libs/hwui/jni/ScopedParcel.cpp
+++ b/libs/hwui/jni/ScopedParcel.cpp
@@ -15,7 +15,7 @@
*/
#include "ScopedParcel.h"
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
using namespace android;
@@ -92,4 +92,4 @@ void ScopedParcel::writeData(const std::optional<sk_sp<SkData>>& optData) {
AParcel_writeByteArray(mParcel, nullptr, -1);
}
}
-#endif // __ANDROID__ // Layoutlib does not support parcel
+#endif // __linux__ // Only Linux support parcel
diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h
index f2f138fda43c..f2b793a354d7 100644
--- a/libs/hwui/jni/ScopedParcel.h
+++ b/libs/hwui/jni/ScopedParcel.h
@@ -15,7 +15,7 @@
*/
#include "SkData.h"
-#ifdef __ANDROID__ // Layoutlib does not support parcel
+#ifdef __linux__ // Only Linux support parcel
#include <android-base/unique_fd.h>
#include <android/binder_parcel.h>
#include <android/binder_parcel_jni.h>
@@ -64,4 +64,4 @@ enum class BlobType : int32_t {
ASHMEM,
};
-#endif // __ANDROID__ // Layoutlib does not support parcel \ No newline at end of file
+#endif // __linux__ // Only Linux support parcel
diff --git a/libs/hwui/jni/graphics_jni_helpers.h b/libs/hwui/jni/graphics_jni_helpers.h
index 91db134af18f..ff26ec1771bd 100644
--- a/libs/hwui/jni/graphics_jni_helpers.h
+++ b/libs/hwui/jni/graphics_jni_helpers.h
@@ -21,6 +21,7 @@
#include <nativehelper/JNIPlatformHelp.h>
#include <nativehelper/scoped_local_ref.h>
#include <nativehelper/scoped_utf_chars.h>
+#include <nativehelper/scoped_primitive_array.h>
#include <string>
// Host targets (layoutlib) do not differentiate between regular and critical native methods,