diff options
Diffstat (limited to 'libs')
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, |