summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
author Android Build Coastguard Worker <android-build-coastguard-worker@google.com> 2024-09-16 23:18:36 +0000
committer Android Build Coastguard Worker <android-build-coastguard-worker@google.com> 2024-09-16 23:18:36 +0000
commit66baab9a17e512fdfd65643e628c594690b36bbb (patch)
treea35537b08bb85da97523a0257d0c0fe9303529e8 /libs
parent26b45954c3b649cb9aec3bfd28143323bf0520b0 (diff)
parentd00ccc1124509d7e1cf59e6e59c0e47a5c03c4ca (diff)
Snap for 12373988 from d00ccc1124509d7e1cf59e6e59c0e47a5c03c4ca to 24Q4-release
Change-Id: I2723e2caf6c24e94afe40d8b981c4f5515b96e0b
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/TEST_MAPPING26
-rw-r--r--libs/WindowManager/Shell/res/values-es-rUS/strings.xml3
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings.xml3
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings.xml3
-rw-r--r--libs/WindowManager/Shell/res/values-it/strings.xml3
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java130
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java138
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt71
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt105
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt161
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt111
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java6
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt84
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt636
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt81
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt181
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt182
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt144
35 files changed, 1991 insertions, 323 deletions
diff --git a/libs/WindowManager/Jetpack/src/TEST_MAPPING b/libs/WindowManager/Jetpack/src/TEST_MAPPING
index f8f64001dd24..600c79bb88a4 100644
--- a/libs/WindowManager/Jetpack/src/TEST_MAPPING
+++ b/libs/WindowManager/Jetpack/src/TEST_MAPPING
@@ -1,32 +1,10 @@
{
"presubmit": [
{
- "name": "WMJetpackUnitTests",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.Presubmit"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
+ "name": "WMJetpackUnitTests_Presubmit"
},
{
- "name": "CtsWindowManagerJetpackTestCases",
- "options": [
- {
- "include-annotation": "android.platform.test.annotations.Presubmit"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
+ "name": "CtsWindowManagerJetpackTestCases_Presubmit"
}
],
"imports": [
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
index 8644780dba03..0dd0a99dd9ae 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -73,8 +73,7 @@
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Descartar burbuja"</string>
- <!-- no translation found for bubble_fullscreen_text (1006758103218086231) -->
- <skip />
+ <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Mover a pantalla completa"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar la conversación en burbuja"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chat con burbujas"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como elementos flotantes o burbujas. Presiona para abrir la burbuja. Arrástrala para moverla."</string>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index 9718bf19fcc3..6df154d9233d 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -73,8 +73,7 @@
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Ajustes de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Cerrar burbuja"</string>
- <!-- no translation found for bubble_fullscreen_text (1006758103218086231) -->
- <skip />
+ <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Cambiar a pantalla completa"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"No mostrar conversación en burbuja"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatea con burbujas"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"Las conversaciones nuevas aparecen como iconos flotantes llamados \"burbujas\". Toca una burbuja para abrirla. Arrástrala para moverla."</string>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 63b5994cd707..92f579db10b5 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -73,8 +73,7 @@
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"Réduire <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Fermer la bulle"</string>
- <!-- no translation found for bubble_fullscreen_text (1006758103218086231) -->
- <skip />
+ <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Passer en plein écran"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Ne pas afficher la conversation dans une bulle"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatter en utilisant des bulles"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"Les nouvelles conversations s\'affichent sous forme d\'icônes flottantes ou de bulles. Appuyez sur la bulle pour l\'ouvrir. Faites-la glisser pour la déplacer."</string>
diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml
index 3ba6873617f6..5f9c492bf055 100644
--- a/libs/WindowManager/Shell/res/values-it/strings.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings.xml
@@ -73,8 +73,7 @@
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"comprimi <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Impostazioni <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Ignora bolla"</string>
- <!-- no translation found for bubble_fullscreen_text (1006758103218086231) -->
- <skip />
+ <string name="bubble_fullscreen_text" msgid="1006758103218086231">"Passa a schermo intero"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Non mettere la conversazione nella bolla"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Chatta utilizzando le bolle"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"Le nuove conversazioni vengono mostrate come icone mobili o bolle. Tocca per aprire la bolla. Trascinala per spostarla."</string>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 341ca0eb6bed..3f9711718a27 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -90,6 +90,9 @@ public class DesktopModeStatus {
/** The maximum override density allowed for tasks inside the desktop. */
private static final int DESKTOP_DENSITY_MAX = 1000;
+ /** The number of [WindowDecorViewHost] instances to warm up on system start. */
+ private static final int WINDOW_DECOR_PRE_WARM_SIZE = 2;
+
/**
* Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time.
*
@@ -102,6 +105,14 @@ public class DesktopModeStatus {
private static final String MAX_TASK_LIMIT_SYS_PROP = "persist.wm.debug.desktop_max_task_limit";
/**
+ * Sysprop declaring the number of [WindowDecorViewHost] instances to warm up on system start.
+ *
+ * <p>If it is not defined, then [WINDOW_DECOR_PRE_WARM_SIZE] is used.
+ */
+ private static final String WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP =
+ "persist.wm.debug.desktop_window_decor_pre_warm_size";
+
+ /**
* Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
*/
public static boolean isVeiledResizeEnabled() {
@@ -141,6 +152,12 @@ public class DesktopModeStatus {
context.getResources().getInteger(R.integer.config_maxDesktopWindowingActiveTasks));
}
+ /** The number of [WindowDecorViewHost] instances to warm up on system start. */
+ public static int getWindowDecorPreWarmSize() {
+ return SystemProperties.getInt(WINDOW_DECOR_PRE_WARM_SIZE_SYS_PROP,
+ WINDOW_DECOR_PRE_WARM_SIZE);
+ }
+
/**
* Return {@code true} if the current device supports desktop mode.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING
index f02559f36169..df3a369febbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TEST_MAPPING
@@ -1,32 +1,10 @@
{
"presubmit": [
{
- "name": "WMShellUnitTests",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "include-filter": "com.android.wm.shell.back"
- }
- ]
+ "name": "WMShellUnitTests_shell_back"
},
{
- "name": "CtsWindowManagerDeviceBackNavigation",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "include-filter": "android.server.wm.backnavigation.BackGestureInvokedTest"
- },
- {
- "include-filter": "android.server.wm.backnavigation.BackNavigationTests"
- },
- {
- "include-filter": "android.server.wm.backnavigation.OnBackInvokedCallbackGestureTest"
- }
- ]
+ "name": "CtsWindowManagerDeviceBackNavigation_com_android_wm_shell_back"
}
]
}
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 f03daada4ca0..c4082d9f649c 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
@@ -303,21 +303,29 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
lastSurfacePosition);
} else {
if (!haveSameLeash(mImeSourceControl, imeSourceControl)) {
- applyVisibilityToLeash(imeSourceControl);
-
if (android.view.inputmethod.Flags.refactorInsetsController()) {
pendingImeStartAnimation = true;
+ // The starting point for the IME should be it's previous state
+ // (whether it is initiallyVisible or not)
+ updateImeVisibility(imeSourceControl.isInitiallyVisible());
}
+ applyVisibilityToLeash(imeSourceControl);
}
if (!mImeShowing) {
removeImeSurface(mDisplayId);
}
}
- } else if (!android.view.inputmethod.Flags.refactorInsetsController()
- && mAnimation != null) {
- // we don"t want to cancel the hide animation, when the control is lost, but
- // continue the bar to slide to the end (even without visible IME)
- mAnimation.cancel();
+ } else {
+ if (!android.view.inputmethod.Flags.refactorInsetsController()
+ && mAnimation != null) {
+ // we don't want to cancel the hide animation, when the control is lost, but
+ // continue the bar to slide to the end (even without visible IME)
+ mAnimation.cancel();
+ } else if (android.view.inputmethod.Flags.refactorInsetsController() && mImeShowing
+ && mAnimation == null) {
+ // There is no leash, so the IME cannot be in a showing state
+ updateImeVisibility(false);
+ }
}
// Make mImeSourceControl point to the new control before starting the animation.
@@ -341,7 +349,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
if (android.view.inputmethod.Flags.refactorInsetsController()) {
if (pendingImeStartAnimation) {
- startAnimation(true, true /* forceRestart */);
+ startAnimation(mImeRequestedVisible, true /* forceRestart */);
}
}
}
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 8c7dcf295319..b151c8b7e718 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
@@ -116,6 +116,8 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.viewhost.DefaultWindowDecorViewHostSupplier;
+import com.android.wm.shell.windowdecor.viewhost.PooledWindowDecorViewHostSupplier;
+import com.android.wm.shell.windowdecor.viewhost.ReusableWindowDecorViewHost;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
import dagger.Binds;
@@ -380,8 +382,19 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static WindowDecorViewHostSupplier provideWindowDecorViewHostSupplier(
- @ShellMainThread @NonNull CoroutineScope mainScope) {
- return new DefaultWindowDecorViewHostSupplier(mainScope);
+ @NonNull Context context,
+ @ShellMainThread @NonNull CoroutineScope mainScope,
+ @NonNull ShellInit shellInit) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)
+ && Flags.enableDesktopWindowingScvhCache()) {
+ final int maxPoolSize = DesktopModeStatus.getMaxTaskLimit(context);
+ final int preWarmSize = DesktopModeStatus.getWindowDecorPreWarmSize();
+ return new PooledWindowDecorViewHostSupplier(
+ context, mainScope, shellInit,
+ ReusableWindowDecorViewHost.DefaultFactory.INSTANCE, maxPoolSize, preWarmSize);
+ } else {
+ return new DefaultWindowDecorViewHostSupplier(mainScope);
+ }
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index f40e0bac1b4e..1573291aef63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -358,9 +358,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
if (mode == TRANSIT_CHANGE && change.hasFlags(FLAG_IS_DISPLAY)) {
if (info.getType() == TRANSIT_CHANGE) {
- final int anim = getRotationAnimationHint(change, info, mDisplayController);
+ int anim = getRotationAnimationHint(change, info, mDisplayController);
isSeamlessDisplayChange = anim == ROTATION_ANIMATION_SEAMLESS;
if (!(isSeamlessDisplayChange || anim == ROTATION_ANIMATION_JUMPCUT)) {
+ if (wallpaperTransit != WALLPAPER_TRANSITION_NONE) {
+ anim |= ScreenRotationAnimation.ANIMATION_HINT_HAS_WALLPAPER;
+ }
startRotationAnimation(startTransaction, change, info, anim, animations,
onAnimFinish);
isDisplayRotationAnimationStarted = true;
@@ -826,24 +829,26 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
@NonNull Runnable finishCallback, @NonNull TransactionPool pool,
@NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
@Nullable Rect clipRect, boolean isActivity) {
+ final DefaultAnimationAdapter adapter = new DefaultAnimationAdapter(anim, leash,
+ position, clipRect, cornerRadius, isActivity);
+ buildSurfaceAnimation(animations, anim, finishCallback, pool, mainExecutor, adapter);
+ }
+
+ /** Builds an animator for the surface and adds it to the `animations` list. */
+ static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Animation anim, @NonNull Runnable finishCallback,
+ @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor,
+ @NonNull AnimationAdapter updateListener) {
final SurfaceControl.Transaction transaction = pool.acquire();
+ updateListener.setTransaction(transaction);
final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
- final Transformation transformation = new Transformation();
- final float[] matrix = new float[9];
// Animation length is already expected to be scaled.
va.overrideDurationScale(1.0f);
va.setDuration(anim.computeDurationHint());
- final ValueAnimator.AnimatorUpdateListener updateListener = animation -> {
- final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
-
- applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
- position, cornerRadius, clipRect, isActivity);
- };
va.addUpdateListener(updateListener);
final Runnable finisher = () -> {
- applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
- position, cornerRadius, clipRect, isActivity);
+ updateListener.onAnimationUpdate(va);
pool.release(transaction);
mainExecutor.execute(() -> {
@@ -1007,37 +1012,88 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
|| animType == ANIM_FROM_STYLE;
}
- private static void applyTransformation(long time, SurfaceControl.Transaction t,
- SurfaceControl leash, Animation anim, Transformation tmpTransformation, float[] matrix,
- Point position, float cornerRadius, @Nullable Rect immutableClipRect,
- boolean isActivity) {
- tmpTransformation.clear();
- anim.getTransformation(time, tmpTransformation);
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && anim.getExtensionEdges() != 0x0 && isActivity) {
- t.setEdgeExtensionEffect(leash, anim.getExtensionEdges());
+ /** The animation adapter for buildSurfaceAnimation. */
+ abstract static class AnimationAdapter implements ValueAnimator.AnimatorUpdateListener {
+ @NonNull final SurfaceControl mLeash;
+ @NonNull SurfaceControl.Transaction mTransaction;
+ private Choreographer mChoreographer;
+
+ AnimationAdapter(@NonNull SurfaceControl leash) {
+ mLeash = leash;
}
- if (position != null) {
- tmpTransformation.getMatrix().postTranslate(position.x, position.y);
+
+ void setTransaction(@NonNull SurfaceControl.Transaction transaction) {
+ mTransaction = transaction;
}
- t.setMatrix(leash, tmpTransformation.getMatrix(), matrix);
- t.setAlpha(leash, tmpTransformation.getAlpha());
-
- final Rect clipRect = immutableClipRect == null ? null : new Rect(immutableClipRect);
- Insets extensionInsets = Insets.min(tmpTransformation.getInsets(), Insets.NONE);
- if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) {
- // Clip out any overflowing edge extension
- clipRect.inset(extensionInsets);
- t.setCrop(leash, clipRect);
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animator) {
+ applyTransformation(animator);
+ if (mChoreographer == null) {
+ mChoreographer = Choreographer.getInstance();
+ }
+ mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+ mTransaction.apply();
}
- if (anim.hasRoundedCorners() && cornerRadius > 0 && clipRect != null) {
- // We can only apply rounded corner if a crop is set
- t.setCrop(leash, clipRect);
- t.setCornerRadius(leash, cornerRadius);
+ abstract void applyTransformation(@NonNull ValueAnimator animator);
+ }
+
+ private static class DefaultAnimationAdapter extends AnimationAdapter {
+ final Transformation mTransformation = new Transformation();
+ final float[] mMatrix = new float[9];
+ @NonNull final Animation mAnim;
+ @Nullable final Point mPosition;
+ @Nullable final Rect mClipRect;
+ final float mCornerRadius;
+ final boolean mIsActivity;
+
+ DefaultAnimationAdapter(@NonNull Animation anim, @NonNull SurfaceControl leash,
+ @Nullable Point position, @Nullable Rect clipRect, float cornerRadius,
+ boolean isActivity) {
+ super(leash);
+ mAnim = anim;
+ mPosition = (position != null && (position.x != 0 || position.y != 0))
+ ? position : null;
+ mClipRect = (clipRect != null && !clipRect.isEmpty()) ? clipRect : null;
+ mCornerRadius = cornerRadius;
+ mIsActivity = isActivity;
}
- t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
- t.apply();
+ @Override
+ void applyTransformation(@NonNull ValueAnimator animator) {
+ final long currentPlayTime = Math.min(animator.getDuration(),
+ animator.getCurrentPlayTime());
+ final Transformation transformation = mTransformation;
+ final SurfaceControl.Transaction t = mTransaction;
+ final SurfaceControl leash = mLeash;
+ transformation.clear();
+ mAnim.getTransformation(currentPlayTime, transformation);
+ if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
+ && mIsActivity && mAnim.getExtensionEdges() != 0) {
+ t.setEdgeExtensionEffect(leash, mAnim.getExtensionEdges());
+ }
+ if (mPosition != null) {
+ transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
+ }
+ t.setMatrix(leash, transformation.getMatrix(), mMatrix);
+ t.setAlpha(leash, transformation.getAlpha());
+
+ if (mClipRect != null) {
+ Rect clipRect = mClipRect;
+ final Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
+ if (!extensionInsets.equals(Insets.NONE)) {
+ // Clip out any overflowing edge extension.
+ clipRect = new Rect(mClipRect);
+ clipRect.inset(extensionInsets);
+ t.setCrop(leash, clipRect);
+ }
+ if (mCornerRadius > 0 && mAnim.hasRoundedCorners()) {
+ // Rounded corner can only be applied if a crop is set.
+ t.setCrop(leash, clipRect);
+ t.setCornerRadius(leash, mCornerRadius);
+ }
+ }
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 5802e2ca8133..b9d11a3d0c06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -25,12 +25,9 @@ import static com.android.wm.shell.transition.DefaultTransitionHandler.buildSurf
import static com.android.wm.shell.transition.Transitions.TAG;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
-import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
@@ -38,6 +35,7 @@ import android.util.Slog;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
+import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.window.ScreenCapture;
@@ -74,6 +72,9 @@ import java.util.ArrayList;
*/
class ScreenRotationAnimation {
static final int MAX_ANIMATION_DURATION = 10 * 1000;
+ static final int ANIMATION_HINT_HAS_WALLPAPER = 1 << 8;
+ /** It must cover all WindowManager#ROTATION_ANIMATION_*. */
+ private static final int ANIMATION_TYPE_MASK = 0xff;
private final Context mContext;
private final TransactionPool mTransactionPool;
@@ -81,7 +82,7 @@ class ScreenRotationAnimation {
/** The leash of the changing window container. */
private final SurfaceControl mSurfaceControl;
- private final int mAnimHint;
+ private final int mAnimType;
private final int mStartWidth;
private final int mStartHeight;
private final int mEndWidth;
@@ -98,6 +99,12 @@ class ScreenRotationAnimation {
private SurfaceControl mBackColorSurface;
/** The leash using to animate screenshot layer. */
private final SurfaceControl mAnimLeash;
+ /**
+ * The container with background color for {@link #mSurfaceControl}. It is only created if
+ * {@link #mSurfaceControl} may be translucent. E.g. visible wallpaper with alpha < 1 (dimmed).
+ * That prevents flickering of alpha blending.
+ */
+ private SurfaceControl mBackEffectSurface;
// The current active animation to move from the old to the new rotated
// state. Which animation is run here will depend on the old and new
@@ -115,7 +122,7 @@ class ScreenRotationAnimation {
Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) {
mContext = context;
mTransactionPool = pool;
- mAnimHint = animHint;
+ mAnimType = animHint & ANIMATION_TYPE_MASK;
mSurfaceControl = change.getLeash();
mStartWidth = change.getStartAbsBounds().width();
@@ -170,11 +177,20 @@ class ScreenRotationAnimation {
}
hardwareBuffer.close();
}
+ if ((animHint & ANIMATION_HINT_HAS_WALLPAPER) != 0) {
+ mBackEffectSurface = new SurfaceControl.Builder()
+ .setCallsite("ShellRotationAnimation").setParent(rootLeash)
+ .setEffectLayer().setOpaque(true).setName("BackEffect").build();
+ t.reparent(mSurfaceControl, mBackEffectSurface)
+ .setColor(mBackEffectSurface,
+ new float[] {mStartLuma, mStartLuma, mStartLuma})
+ .show(mBackEffectSurface);
+ }
t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
t.show(mAnimLeash);
// Crop the real content in case it contains a larger child layer, e.g. wallpaper.
- t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
+ t.setCrop(getEnterSurface(), new Rect(0, 0, mEndWidth, mEndHeight));
if (!isCustomRotate()) {
mBackColorSurface = new SurfaceControl.Builder()
@@ -199,7 +215,12 @@ class ScreenRotationAnimation {
}
private boolean isCustomRotate() {
- return mAnimHint == ROTATION_ANIMATION_CROSSFADE || mAnimHint == ROTATION_ANIMATION_JUMPCUT;
+ return mAnimType == ROTATION_ANIMATION_CROSSFADE || mAnimType == ROTATION_ANIMATION_JUMPCUT;
+ }
+
+ /** Returns the surface which contains the real content to animate enter. */
+ private SurfaceControl getEnterSurface() {
+ return mBackEffectSurface != null ? mBackEffectSurface : mSurfaceControl;
}
private void setScreenshotTransform(SurfaceControl.Transaction t) {
@@ -260,7 +281,7 @@ class ScreenRotationAnimation {
final boolean customRotate = isCustomRotate();
if (customRotate) {
mRotateExitAnimation = AnimationUtils.loadAnimation(mContext,
- mAnimHint == ROTATION_ANIMATION_JUMPCUT ? R.anim.rotation_animation_jump_exit
+ mAnimType == ROTATION_ANIMATION_JUMPCUT ? R.anim.rotation_animation_jump_exit
: R.anim.rotation_animation_xfade_exit);
mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext,
R.anim.rotation_animation_enter);
@@ -314,7 +335,11 @@ class ScreenRotationAnimation {
} else {
startDisplayRotation(animations, finishCallback, mainExecutor);
startScreenshotRotationAnimation(animations, finishCallback, mainExecutor);
- //startColorAnimation(mTransaction, animationScale);
+ if (mBackEffectSurface != null && mStartLuma > 0.1f) {
+ // Animate from the color of background to black for smooth alpha blending.
+ buildLumaAnimation(animations, mStartLuma, 0f /* endLuma */, mBackEffectSurface,
+ animationScale, finishCallback, mainExecutor);
+ }
}
return true;
@@ -322,7 +347,7 @@ class ScreenRotationAnimation {
private void startDisplayRotation(@NonNull ArrayList<Animator> animations,
@NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
- buildSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback,
+ buildSurfaceAnimation(animations, mRotateEnterAnimation, getEnterSurface(), finishCallback,
mTransactionPool, mainExecutor, null /* position */, 0 /* cornerRadius */,
null /* clipRect */, false /* isActivity */);
}
@@ -341,40 +366,17 @@ class ScreenRotationAnimation {
null /* clipRect */, false /* isActivity */);
}
- private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) {
- int colorTransitionMs = mContext.getResources().getInteger(
- R.integer.config_screen_rotation_color_transition);
- final float[] rgbTmpFloat = new float[3];
- final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma);
- final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma);
- final long duration = colorTransitionMs * (long) animationScale;
- final Transaction t = mTransactionPool.acquire();
-
- final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
- // Animation length is already expected to be scaled.
- va.overrideDurationScale(1.0f);
- va.setDuration(duration);
- va.addUpdateListener(animation -> {
- final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
- final float fraction = currentPlayTime / va.getDuration();
- applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t);
- });
- va.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
- t);
- mTransactionPool.release(t);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface,
- t);
- mTransactionPool.release(t);
- }
- });
- animExecutor.execute(va::start);
+ private void buildLumaAnimation(@NonNull ArrayList<Animator> animations,
+ float startLuma, float endLuma, SurfaceControl surface, float animationScale,
+ @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor) {
+ final long durationMillis = (long) (mContext.getResources().getInteger(
+ R.integer.config_screen_rotation_color_transition) * animationScale);
+ final LumaAnimation animation = new LumaAnimation(durationMillis);
+ // Align the end with the enter animation.
+ animation.setStartOffset(mRotateEnterAnimation.getDuration() - durationMillis);
+ final LumaAnimationAdapter adapter = new LumaAnimationAdapter(surface, startLuma, endLuma);
+ buildSurfaceAnimation(animations, animation, finishCallback, mTransactionPool,
+ mainExecutor, adapter);
}
public void kill() {
@@ -389,21 +391,47 @@ class ScreenRotationAnimation {
if (mBackColorSurface != null && mBackColorSurface.isValid()) {
t.remove(mBackColorSurface);
}
+ if (mBackEffectSurface != null && mBackEffectSurface.isValid()) {
+ t.remove(mBackEffectSurface);
+ }
t.apply();
mTransactionPool.release(t);
}
- private static void applyColor(int startColor, int endColor, float[] rgbFloat,
- float fraction, SurfaceControl surface, SurfaceControl.Transaction t) {
- final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor,
- endColor);
- Color middleColor = Color.valueOf(color);
- rgbFloat[0] = middleColor.red();
- rgbFloat[1] = middleColor.green();
- rgbFloat[2] = middleColor.blue();
- if (surface.isValid()) {
- t.setColor(surface, rgbFloat);
+ /** A no-op wrapper to provide animation duration. */
+ private static class LumaAnimation extends Animation {
+ LumaAnimation(long durationMillis) {
+ setDuration(durationMillis);
+ }
+ }
+
+ private static class LumaAnimationAdapter extends DefaultTransitionHandler.AnimationAdapter {
+ final float[] mColorArray = new float[3];
+ final float mStartLuma;
+ final float mEndLuma;
+ final AccelerateInterpolator mInterpolation;
+
+ LumaAnimationAdapter(@NonNull SurfaceControl leash, float startLuma, float endLuma) {
+ super(leash);
+ mStartLuma = startLuma;
+ mEndLuma = endLuma;
+ // Make the initial progress color lighter if the background is light. That avoids
+ // darker content when fading into the entering surface.
+ final float factor = Math.min(3f, (Math.max(0.5f, mStartLuma) - 0.5f) * 10);
+ Slog.d(TAG, "Luma=" + mStartLuma + " factor=" + factor);
+ mInterpolation = factor > 0.5f ? new AccelerateInterpolator(factor) : null;
+ }
+
+ @Override
+ void applyTransformation(ValueAnimator animator) {
+ final float fraction = mInterpolation != null
+ ? mInterpolation.getInterpolation(animator.getAnimatedFraction())
+ : animator.getAnimatedFraction();
+ final float luma = mStartLuma + fraction * (mEndLuma - mStartLuma);
+ mColorArray[0] = luma;
+ mColorArray[1] = luma;
+ mColorArray[2] = luma;
+ mTransaction.setColor(mLeash, mColorArray);
}
- t.apply();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 46fe68f44bed..0caa8e93d4eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -55,6 +55,7 @@ import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.desktopmode.DesktopModeFlags;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
import com.android.wm.shell.windowdecor.viewhost.WindowDecorViewHostSupplier;
@@ -240,7 +241,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) {
final boolean isFreeform =
taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM;
- final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
+ final boolean isDragResizeable = DesktopModeFlags.SCALED_RESIZING.isEnabled(mContext)
+ ? isFreeform : isFreeform && taskInfo.isResizeable;
final WindowDecorLinearLayout oldRootView = mResult.mRootView;
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
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 2519ce4e6571..b14283f878a3 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
@@ -413,7 +413,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo;
if (taskInfo.displayId != oldTaskInfo.displayId
- && !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ && !Flags.enableHandleInputFix()) {
removeTaskFromEventReceiver(oldTaskInfo.displayId);
incrementEventReceiverTasks(taskInfo.displayId);
}
@@ -480,7 +480,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
decoration.close();
final int displayId = taskInfo.displayId;
if (mEventReceiversByDisplay.contains(displayId)
- && !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ && !Flags.enableHandleInputFix()) {
removeTaskFromEventReceiver(displayId);
}
// Remove the decoration from the cache last because WindowDecoration#close could still
@@ -1096,7 +1096,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
relevantDecor.updateHoverAndPressStatus(ev);
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- if (!mTransitionDragActive && !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ if (!mTransitionDragActive && !Flags.enableHandleInputFix()) {
relevantDecor.closeHandleMenuIfNeeded(ev);
}
}
@@ -1139,7 +1139,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
final boolean shouldStartTransitionDrag =
relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
- || Flags.enableAdditionalWindowsAboveStatusBar();
+ || Flags.enableHandleInputFix();
if (dragFromStatusBarAllowed && shouldStartTransitionDrag) {
mTransitionDragActive = true;
}
@@ -1424,7 +1424,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
windowDecoration.relayout(taskInfo, startT, finishT,
false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */);
- if (!Flags.enableAdditionalWindowsAboveStatusBar()) {
+ if (!Flags.enableHandleInputFix()) {
incrementEventReceiverTasks(taskInfo.displayId);
}
}
@@ -1580,7 +1580,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& Flags.enableDesktopWindowingImmersiveHandleHiding()) {
decor.onInsetsStateChanged(insetsState);
}
- if (!Flags.enableAdditionalWindowsAboveStatusBar()) {
+ if (!Flags.enableHandleInputFix()) {
// If status bar inset is visible, top task is not in immersive mode.
// This value is only needed when the App Handle input is being handled
// through the global input monitor (hence the flag check) to ignore gestures
@@ -1603,14 +1603,27 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
Transitions transitions,
InteractionJankMonitor interactionJankMonitor,
Supplier<SurfaceControl.Transaction> transactionFactory) {
- if (!DesktopModeStatus.isVeiledResizeEnabled()) {
- return new FluidResizeTaskPositioner(
- taskOrganizer, transitions, windowDecoration, displayController,
- dragStartListener, transactionFactory);
+ final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled()
+ ? new VeiledResizeTaskPositioner(
+ taskOrganizer,
+ windowDecoration,
+ displayController,
+ dragStartListener,
+ transitions,
+ interactionJankMonitor)
+ : new FluidResizeTaskPositioner(
+ taskOrganizer,
+ transitions,
+ windowDecoration,
+ displayController,
+ dragStartListener,
+ transactionFactory);
+
+ if (DesktopModeFlags.SCALED_RESIZING.isEnabled(windowDecoration.mContext)) {
+ return new FixedAspectRatioTaskPositionerDecorator(windowDecoration,
+ taskPositioner);
}
- return new VeiledResizeTaskPositioner(
- taskOrganizer, windowDecoration, displayController,
- dragStartListener, transitions, interactionJankMonitor);
+ return taskPositioner;
}
}
}
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 5d16d972a0f2..55da78e74ba2 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
@@ -455,7 +455,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
- if (!isDragResizable(mTaskInfo)) {
+ if (!isDragResizable(mTaskInfo, mContext)) {
if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
// We still want to track caption bar's exclusion region on a non-resizeable task.
updateExclusionRegion();
@@ -497,12 +497,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
}
- private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo,
+ Context context) {
+ if (DesktopModeFlags.SCALED_RESIZING.isEnabled(context)) {
+ return taskInfo.isFreeform();
+ }
return taskInfo.isFreeform() && taskInfo.isResizeable;
}
private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
- if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+ if (!isDragResizable(mTaskInfo, mContext) || !isMaximizeMenuActive()) {
return;
}
if (!mTaskInfo.isVisible()) {
@@ -532,7 +536,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
void disposeStatusBarInputLayer() {
if (!isAppHandle(mWindowDecorViewHolder)
- || !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ || !Flags.enableHandleInputFix()) {
return;
}
((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer();
@@ -628,7 +632,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END;
relayoutParams.mOccludingCaptionElements.add(controlsElement);
- } else if (isAppHandle && !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ } else if (isAppHandle && !Flags.enableHandleInputFix()) {
// The focused decor (fullscreen/split) does not need to handle input because input in
// the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel.
// Note: This does not apply with the above flag enabled as the status bar input layer
@@ -1153,13 +1157,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder)
- || Flags.enableAdditionalWindowsAboveStatusBar()) {
+ || Flags.enableHandleInputFix()) {
return false;
}
// The status bar input layer can only receive input in handle coordinates to begin with,
// so checking coordinates is unnecessary as input is always within handle bounds.
if (isAppHandle(mWindowDecorViewHolder)
- && Flags.enableAdditionalWindowsAboveStatusBar()
+ && Flags.enableHandleInputFix()
&& isCaptionVisible()) {
return true;
}
@@ -1196,7 +1200,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* @param ev the MotionEvent to compare
*/
void checkTouchEvent(MotionEvent ev) {
- if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return;
+ if (mResult.mRootView == null || Flags.enableHandleInputFix()) return;
final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
final View handle = caption.findViewById(R.id.caption_handle);
final boolean inHandle = !isHandleMenuActive()
@@ -1209,7 +1213,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// If the whole handle menu can be touched directly, rely on FLAG_WATCH_OUTSIDE_TOUCH.
// This is for the case that some of the handle menu is underneath the status bar.
if (isAppHandle(mWindowDecorViewHolder)
- && !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ && !Flags.enableHandleInputFix()) {
mHandleMenu.checkMotionEvent(ev);
closeHandleMenuIfNeeded(ev);
}
@@ -1223,7 +1227,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* @param ev the MotionEvent to compare against.
*/
void updateHoverAndPressStatus(MotionEvent ev) {
- if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return;
+ if (mResult.mRootView == null || Flags.enableHandleInputFix()) return;
final View handle = mResult.mRootView.findViewById(R.id.caption_handle);
final boolean inHandle = !isHandleMenuActive()
&& checkTouchEventInFocusedCaptionHandle(ev);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index cb9781e86c87..cad34621c82a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -84,22 +84,47 @@ public class DragPositioningCallbackUtility {
repositionTaskBounds.set(taskBoundsAtDragStart);
+ boolean isAspectRatioMaintained = true;
// Make sure the new resizing destination in any direction falls within the stable bounds.
if ((ctrlType & CTRL_TYPE_LEFT) != 0) {
repositionTaskBounds.left = Math.max(repositionTaskBounds.left + (int) delta.x,
stableBounds.left);
+ if (repositionTaskBounds.left == stableBounds.left
+ && repositionTaskBounds.left + (int) delta.x != stableBounds.left) {
+ // If the task edge have been set to the stable bounds and not due to the users
+ // drag, the aspect ratio of the task will not be maintained.
+ isAspectRatioMaintained = false;
+ }
}
if ((ctrlType & CTRL_TYPE_RIGHT) != 0) {
repositionTaskBounds.right = Math.min(repositionTaskBounds.right + (int) delta.x,
stableBounds.right);
+ if (repositionTaskBounds.right == stableBounds.right
+ && repositionTaskBounds.right + (int) delta.x != stableBounds.right) {
+ // If the task edge have been set to the stable bounds and not due to the users
+ // drag, the aspect ratio of the task will not be maintained.
+ isAspectRatioMaintained = false;
+ }
}
if ((ctrlType & CTRL_TYPE_TOP) != 0) {
repositionTaskBounds.top = Math.max(repositionTaskBounds.top + (int) delta.y,
stableBounds.top);
+ if (repositionTaskBounds.top == stableBounds.top
+ && repositionTaskBounds.top + (int) delta.y != stableBounds.top) {
+ // If the task edge have been set to the stable bounds and not due to the users
+ // drag, the aspect ratio of the task will not be maintained.
+ isAspectRatioMaintained = false;
+ }
}
if ((ctrlType & CTRL_TYPE_BOTTOM) != 0) {
repositionTaskBounds.bottom = Math.min(repositionTaskBounds.bottom + (int) delta.y,
stableBounds.bottom);
+ if (repositionTaskBounds.bottom == stableBounds.bottom
+ && repositionTaskBounds.bottom + (int) delta.y != stableBounds.bottom) {
+ // If the task edge have been set to the stable bounds and not due to the users
+ // drag, the aspect ratio of the task will not be maintained.
+ isAspectRatioMaintained = false;
+ }
}
// If width or height are negative or exceeding the width or height constraints, revert the
@@ -108,11 +133,24 @@ public class DragPositioningCallbackUtility {
windowDecoration)) {
repositionTaskBounds.right = oldRight;
repositionTaskBounds.left = oldLeft;
+ isAspectRatioMaintained = false;
}
if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController,
windowDecoration)) {
repositionTaskBounds.top = oldTop;
repositionTaskBounds.bottom = oldBottom;
+ isAspectRatioMaintained = false;
+ }
+
+ // If the application is unresizeable and any bounds have been set back to their old
+ // location or to a stable bound edge, reset all the bounds to maintain the applications
+ // aspect ratio.
+ if (DesktopModeFlags.SCALED_RESIZING.isEnabled(windowDecoration.mDecorWindowContext)
+ && !isAspectRatioMaintained && !windowDecoration.mTaskInfo.isResizeable) {
+ repositionTaskBounds.top = oldTop;
+ repositionTaskBounds.bottom = oldBottom;
+ repositionTaskBounds.right = oldRight;
+ repositionTaskBounds.left = oldLeft;
}
// If there are no changes to the bounds after checking new bounds against minimum and
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt
index e8131a00ba40..3885761d0742 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt
@@ -16,8 +16,10 @@
package com.android.wm.shell.windowdecor
+import android.app.ActivityManager.RunningTaskInfo
import android.graphics.PointF
import android.graphics.Rect
+import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
@@ -51,8 +53,7 @@ class FixedAspectRatioTaskPositionerDecorator (
return super.onDragPositioningStart(originalCtrlType, x, y)
}
- lastRepositionedBounds.set(
- windowDecoration.mTaskInfo.configuration.windowConfiguration.bounds)
+ lastRepositionedBounds.set(getBounds(windowDecoration.mTaskInfo))
startingPoint.set(x, y)
lastValidPoint.set(x, y)
val startingBoundWidth = lastRepositionedBounds.width()
@@ -255,4 +256,9 @@ class FixedAspectRatioTaskPositionerDecorator (
private fun requiresFixedAspectRatio(): Boolean {
return originalCtrlType.isResizing() && !windowDecoration.mTaskInfo.isResizeable
}
+
+ @VisibleForTesting
+ fun getBounds(taskInfo: RunningTaskInfo): Rect {
+ return taskInfo.configuration.windowConfiguration.bounds
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
index 3d00a445d9e0..faffe4a07d63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt
@@ -81,7 +81,7 @@ class HandleMenu(
private val taskInfo: RunningTaskInfo = parentDecor.mTaskInfo
private val isViewAboveStatusBar: Boolean
- get() = (Flags.enableAdditionalWindowsAboveStatusBar() && !taskInfo.isFreeform)
+ get() = (Flags.enableHandleInputFix() && !taskInfo.isFreeform)
private val pillElevation: Int = loadDimensionPixelSize(
R.dimen.desktop_mode_handle_menu_pill_elevation)
@@ -183,7 +183,7 @@ class HandleMenu(
val x = handleMenuPosition.x.toInt()
val y = handleMenuPosition.y.toInt()
handleMenuViewContainer =
- if (!taskInfo.isFreeform && Flags.enableAdditionalWindowsAboveStatusBar()) {
+ if (!taskInfo.isFreeform && Flags.enableHandleInputFix()) {
AdditionalSystemViewContainer(
windowManagerWrapper = windowManagerWrapper,
taskId = taskInfo.taskId,
@@ -218,7 +218,7 @@ class HandleMenu(
menuX = marginMenuStart
menuY = marginMenuTop
} else {
- if (Flags.enableAdditionalWindowsAboveStatusBar()) {
+ if (Flags.enableHandleInputFix()) {
// In a focused decor, we use global coordinates for handle menu. Therefore we
// need to account for other factors like split stage and menu/handle width to
// center the menu.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
index 18757ef6ff40..cf82bb4f9919 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
@@ -39,7 +39,7 @@ class HandleMenuImageButton(
lateinit var taskInfo: RunningTaskInfo
override fun onHoverEvent(motionEvent: MotionEvent): Boolean {
- if (Flags.enableAdditionalWindowsAboveStatusBar() || taskInfo.isFreeform) {
+ if (Flags.enableHandleInputFix() || taskInfo.isFreeform) {
return super.onHoverEvent(motionEvent)
} else {
return false
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 9ef4b8cde8ef..1c11a8dfbbb4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -100,7 +100,7 @@ internal class AppHandleViewHolder(
private fun createStatusBarInputLayer(handlePosition: Point,
handleWidth: Int,
handleHeight: Int) {
- if (!Flags.enableAdditionalWindowsAboveStatusBar()) return
+ if (!Flags.enableHandleInputFix()) return
statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper,
taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
index 139e6790b744..5156e47cfd13 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHost.kt
@@ -19,51 +19,33 @@ import android.content.Context
import android.content.res.Configuration
import android.view.Display
import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
-import android.view.WindowlessWindowManager
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.shared.annotations.ShellMainThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-typealias SurfaceControlViewHostFactory =
- (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost
/**
- * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHost].
+ * A default implementation of [WindowDecorViewHost] backed by a [SurfaceControlViewHostAdapter].
*
- * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
- * any attempts to do will throw, which means that once a [View] is added using [updateView] or
- * [updateViewAsync], only its properties and binding may be changed, its children views may be
- * added, removed or changed and its [WindowManager.LayoutParams] may be changed.
- * It also supports asynchronously updating the view hierarchy using [updateViewAsync], in which
+ * It supports asynchronously updating the view hierarchy using [updateViewAsync], in which
* case the update work will be posted on the [ShellMainThread] with no delay.
*/
class DefaultWindowDecorViewHost(
- private val context: Context,
+ context: Context,
@ShellMainThread private val mainScope: CoroutineScope,
- private val display: Display,
- private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
- SurfaceControlViewHost(c, d, wwm, s)
- }
+ display: Display,
+ @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
+ SurfaceControlViewHostAdapter(context, display)
) : WindowDecorViewHost {
- private val rootSurface: SurfaceControl = SurfaceControl.Builder()
- .setName("DefaultWindowDecorViewHost surface")
- .setContainerLayer()
- .setCallsite("DefaultWindowDecorViewHost#init")
- .build()
-
- private var wwm: WindowlessWindowManager? = null
- @VisibleForTesting
- var viewHost: SurfaceControlViewHost? = null
private var currentUpdateJob: Job? = null
override val surfaceControl: SurfaceControl
- get() = rootSurface
+ get() = viewHostAdapter.rootSurface
override fun updateView(
view: View,
@@ -92,8 +74,7 @@ class DefaultWindowDecorViewHost(
override fun release(t: SurfaceControl.Transaction) {
clearCurrentUpdateJob()
- viewHost?.release()
- t.remove(rootSurface)
+ viewHostAdapter.release(t)
}
private fun updateViewHost(
@@ -102,45 +83,15 @@ class DefaultWindowDecorViewHost(
configuration: Configuration,
onDrawTransaction: SurfaceControl.Transaction?
) {
- Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost")
- if (wwm == null) {
- wwm = WindowlessWindowManager(configuration, rootSurface, null)
- }
- requireWindowlessWindowManager().setConfiguration(configuration)
- if (viewHost == null) {
- viewHost = surfaceControlViewHostFactory.invoke(
- context,
- display,
- requireWindowlessWindowManager(),
- "DefaultWindowDecorViewHost#updateViewHost"
- )
- }
+ viewHostAdapter.prepareViewHost(configuration)
onDrawTransaction?.let {
- requireViewHost().rootSurfaceControl.applyTransactionOnDraw(it)
- }
- if (requireViewHost().view == null) {
- Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-setView")
- requireViewHost().setView(view, attrs)
- Trace.endSection()
- } else {
- check(requireViewHost().view == view) { "Changing view is not allowed" }
- Trace.beginSection("DefaultWindowDecorViewHost#updateViewHost-relayout")
- requireViewHost().relayout(attrs)
- Trace.endSection()
+ viewHostAdapter.applyTransactionOnDraw(it)
}
- Trace.endSection()
+ viewHostAdapter.updateView(view, attrs)
}
private fun clearCurrentUpdateJob() {
currentUpdateJob?.cancel()
currentUpdateJob = null
}
-
- private fun requireWindowlessWindowManager(): WindowlessWindowManager {
- return wwm ?: error("Expected non-null windowless window manager")
- }
-
- private fun requireViewHost(): SurfaceControlViewHost {
- return viewHost ?: error("Expected non-null view host")
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt
new file mode 100644
index 000000000000..b04188fa82a8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplier.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+import android.content.Context
+import android.os.Trace
+import android.util.Pools
+import android.view.Display
+import android.view.SurfaceControl
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.sysui.ShellInit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * A [WindowDecorViewHostSupplier] backed by a pool to allow recycling view hosts which may be
+ * expensive to recreate for each new/updated window decoration.
+ *
+ * Callers can obtain [ReusableWindowDecorViewHost] using [acquire], which will return a pooled
+ * object if available, or create a new instance and return it if needed. When done using a
+ * [ReusableWindowDecorViewHost], it must be released using [release] to allow it to be sent back
+ * into the pool and reused later on.
+ *
+ * This class also supports pre-warming [ReusableWindowDecorViewHost] instances, which will be put
+ * into the pool immediately after creation.
+ */
+class PooledWindowDecorViewHostSupplier(
+ private val context: Context,
+ @ShellMainThread private val mainScope: CoroutineScope,
+ shellInit: ShellInit,
+ private val viewHostFactory: ReusableWindowDecorViewHost.Factory =
+ ReusableWindowDecorViewHost.DefaultFactory,
+ maxPoolSize: Int,
+ private val preWarmSize: Int,
+) : WindowDecorViewHostSupplier<ReusableWindowDecorViewHost> {
+
+ private val pool: Pools.Pool<ReusableWindowDecorViewHost> = Pools.SynchronizedPool(maxPoolSize)
+ private var nextDecorViewHostId = 0
+
+ init {
+ require(preWarmSize <= maxPoolSize) { "Pre-warm size should not exceed pool size" }
+ shellInit.addInitCallback(this::onShellInit, this)
+ }
+
+ private fun onShellInit() {
+ if (preWarmSize <= 0) {
+ return
+ }
+ preWarmViewHosts(preWarmSize)
+ }
+
+ private fun preWarmViewHosts(preWarmSize: Int) {
+ mainScope.launch {
+ // Applying isn't needed, as the surface was never actually shown.
+ val t = SurfaceControl.Transaction()
+ repeat(preWarmSize) {
+ val warmedViewHost = create(context, context.display).apply {
+ warmUp()
+ }
+ // Put the warmed view host in the pool by releasing it.
+ release(warmedViewHost, t)
+ }
+ }
+ }
+
+ override fun acquire(context: Context, display: Display): ReusableWindowDecorViewHost {
+ val reusedDecorViewHost = pool.acquire()
+ if (reusedDecorViewHost != null) {
+ return reusedDecorViewHost
+ }
+ Trace.beginSection("WindowDecorViewHostPool#acquire-new")
+ val newDecorViewHost = create(context, display)
+ Trace.endSection()
+ return newDecorViewHost
+ }
+
+ override fun release(viewHost: ReusableWindowDecorViewHost, t: SurfaceControl.Transaction) {
+ val cached = pool.release(viewHost)
+ if (!cached) {
+ viewHost.release(t)
+ }
+ }
+
+ private fun create(context: Context, display: Display): ReusableWindowDecorViewHost {
+ return viewHostFactory.create(
+ context,
+ mainScope,
+ display,
+ nextDecorViewHostId++
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt
new file mode 100644
index 000000000000..64536d1a7897
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHost.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.PixelFormat
+import android.os.Trace
+import android.view.Display
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+import android.view.WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION
+import android.widget.FrameLayout
+import com.android.internal.annotations.VisibleForTesting
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * An implementation of [WindowDecorViewHost] that supports:
+ * 1) Replacing the root [View], meaning [WindowDecorViewHost.updateView] maybe be
+ * called with different [View] instances. This is useful when reusing [WindowDecorViewHost]s
+ * instances for vastly different view hierarchies, such as Desktop Windowing's App Handles and
+ * App Headers.
+ * 2) Pre-warming of the underlying [SurfaceControlViewHost]s. Useful because their creation and
+ * first root view assignment are expensive, which is undesirable in latency-sensitive code
+ * paths like during a shell transition.
+ */
+class ReusableWindowDecorViewHost(
+ private val context: Context,
+ @ShellMainThread private val mainScope: CoroutineScope,
+ display: Display,
+ val id: Int,
+ @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter =
+ SurfaceControlViewHostAdapter(context, display)
+) : WindowDecorViewHost, Warmable {
+
+ @VisibleForTesting
+ val rootView = FrameLayout(context)
+
+ private var currentUpdateJob: Job? = null
+
+ override val surfaceControl: SurfaceControl
+ get() = viewHostAdapter.rootSurface
+
+ override fun warmUp() {
+ if (viewHostAdapter.isInitialized()) {
+ // Already warmed up.
+ return
+ }
+ Trace.beginSection("$TAG#warmUp")
+ viewHostAdapter.prepareViewHost(context.resources.configuration)
+ viewHostAdapter.updateView(
+ rootView,
+ WindowManager.LayoutParams(
+ 0 /* width*/,
+ 0 /* height */,
+ TYPE_APPLICATION,
+ FLAG_NOT_FOCUSABLE or FLAG_SPLIT_TOUCH,
+ PixelFormat.TRANSPARENT
+ ).apply {
+ setTitle("View root of $TAG#$id")
+ setTrustedOverlay()
+ }
+ )
+ Trace.endSection()
+ }
+
+ override fun updateView(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ onDrawTransaction: SurfaceControl.Transaction?
+ ) {
+ clearCurrentUpdateJob()
+ updateViewHost(view, attrs, configuration, onDrawTransaction)
+ }
+
+ override fun updateViewAsync(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration
+ ) {
+ clearCurrentUpdateJob()
+ currentUpdateJob = mainScope.launch {
+ updateViewHost(view, attrs, configuration, onDrawTransaction = null)
+ }
+ }
+
+ override fun release(t: SurfaceControl.Transaction) {
+ clearCurrentUpdateJob()
+ viewHostAdapter.release(t)
+ }
+
+ private fun updateViewHost(
+ view: View,
+ attrs: WindowManager.LayoutParams,
+ configuration: Configuration,
+ onDrawTransaction: SurfaceControl.Transaction?
+ ) {
+ viewHostAdapter.prepareViewHost(configuration)
+ onDrawTransaction?.let {
+ viewHostAdapter.applyTransactionOnDraw(it)
+ }
+ rootView.removeAllViews()
+ rootView.addView(view)
+ viewHostAdapter.updateView(rootView, attrs)
+ }
+
+ private fun clearCurrentUpdateJob() {
+ currentUpdateJob?.cancel()
+ currentUpdateJob = null
+ }
+
+ interface Factory {
+ fun create(
+ context: Context,
+ @ShellMainThread mainScope: CoroutineScope,
+ display: Display,
+ id: Int
+ ): ReusableWindowDecorViewHost
+ }
+
+ object DefaultFactory : Factory {
+ override fun create(
+ context: Context,
+ @ShellMainThread mainScope: CoroutineScope,
+ display: Display,
+ id: Int
+ ): ReusableWindowDecorViewHost {
+ return ReusableWindowDecorViewHost(
+ context,
+ mainScope,
+ display,
+ id
+ )
+ }
+ }
+
+ companion object {
+ private const val TAG = "ReusableWindowDecorViewHost"
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt
new file mode 100644
index 000000000000..a54c9ba67cf8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapter.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.AttachedSurfaceControl
+import android.view.Display
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.WindowManager
+import android.view.WindowlessWindowManager
+import androidx.tracing.Trace
+import com.android.internal.annotations.VisibleForTesting
+typealias SurfaceControlViewHostFactory =
+ (Context, Display, WindowlessWindowManager, String) -> SurfaceControlViewHost
+
+/**
+ * Adapter for a [SurfaceControlViewHost] and its backing [SurfaceControl].
+ *
+ * It does not support swapping the root view added to the VRI of the [SurfaceControlViewHost], and
+ * any attempts to do will throw, which means that once a [View] is added using [updateView], only
+ * its properties and binding may be changed, its children views may be added, removed or changed
+ * and its [WindowManager.LayoutParams] may be changed.
+ */
+class SurfaceControlViewHostAdapter(
+ private val context: Context,
+ private val display: Display,
+ private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = { c, d, wwm, s ->
+ SurfaceControlViewHost(c, d, wwm, s)
+ }
+) {
+ val rootSurface: SurfaceControl = SurfaceControl.Builder()
+ .setName("SurfaceControlViewHostAdapter surface")
+ .setContainerLayer()
+ .setCallsite("SurfaceControlViewHostAdapter#init")
+ .build()
+
+ private var wwm: WindowlessWindowManager? = null
+ @VisibleForTesting
+ var viewHost: SurfaceControlViewHost? = null
+
+ /** Initialize the [SurfaceControlViewHost] if needed. */
+ fun prepareViewHost(configuration: Configuration) {
+ if (wwm == null) {
+ wwm = WindowlessWindowManager(configuration, rootSurface, null)
+ }
+ requireWindowlessWindowManager().setConfiguration(configuration)
+ if (viewHost == null) {
+ viewHost = surfaceControlViewHostFactory.invoke(
+ context,
+ display,
+ requireWindowlessWindowManager(),
+ "SurfaceControlViewHostAdapter#prepareViewHost"
+ )
+ }
+ }
+
+ /**
+ * Request to apply the transaction atomically with the next draw of the view hierarchy.
+ * See [AttachedSurfaceControl.applyTransactionOnDraw].
+ */
+ fun applyTransactionOnDraw(t: SurfaceControl.Transaction) {
+ requireViewHost().rootSurfaceControl.applyTransactionOnDraw(t)
+ }
+
+ /** Update the view hierarchy of the view host. */
+ fun updateView(view: View, attrs: WindowManager.LayoutParams) {
+ if (requireViewHost().view == null) {
+ Trace.beginSection("SurfaceControlViewHostAdapter#updateView-setView")
+ requireViewHost().setView(view, attrs)
+ Trace.endSection()
+ } else {
+ check(requireViewHost().view == view) { "Changing view is not allowed" }
+ Trace.beginSection("SurfaceControlViewHostAdapter#updateView-relayout")
+ requireViewHost().relayout(attrs)
+ Trace.endSection()
+ }
+ }
+
+ /** Release the view host and remove the backing surface. */
+ fun release(t: SurfaceControl.Transaction) {
+ viewHost?.release()
+ t.remove(rootSurface)
+ }
+
+ /** Whether the view host has had a view hierarchy set. */
+ fun isInitialized(): Boolean = viewHost?.view != null
+
+ private fun requireWindowlessWindowManager(): WindowlessWindowManager {
+ return wwm ?: error("Expected non-null windowless window manager")
+ }
+
+ private fun requireViewHost(): SurfaceControlViewHost {
+ return viewHost ?: error("Expected non-null view host")
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt
new file mode 100644
index 000000000000..0df9bfa2ee78
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewhost/Warmable.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+/**
+ * An interface for an object that can be warmed up before it's needed.
+ */
+interface Warmable {
+ fun warmUp()
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index a17d08d8fbfb..a18fbf0891ef 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -316,7 +316,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
fun testCreateAndDisposeEventReceiver() {
val decor = createOpenTaskDecoration(windowingMode = WINDOWING_MODE_FREEFORM)
desktopModeWindowDecorViewModel.destroyWindowDecoration(decor.mTaskInfo)
@@ -326,7 +326,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
fun testEventReceiversOnMultipleDisplays() {
val secondaryDisplay = createVirtualDisplay() ?: return
val secondaryDisplayId = secondaryDisplay.display.displayId
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 1741fe447fad..92199a11e837 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -107,6 +107,7 @@ import kotlin.jvm.functions.Function1;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -406,7 +407,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
public void updateRelayoutParams_fullscreen_inputChannelNotNeeded() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
@@ -423,7 +424,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @DisableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
public void updateRelayoutParams_multiwindow_inputChannelNotNeeded() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
@@ -563,6 +564,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
+ @Ignore("TODO(b/367235906): Due to MONITOR_INPUT permission error")
public void relayout_freeformTask_appliesTransactionOnDraw() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index 1f33ae69b724..24f6becc3536 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -39,6 +39,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
@@ -105,6 +106,7 @@ class DragPositioningCallbackUtilityTest {
initializeTaskInfo()
mockWindowDecoration.mDisplay = mockDisplay
mockWindowDecoration.mDecorWindowContext = mockContext
+ mockWindowDecoration.mTaskInfo.isResizeable = true
whenever(mockContext.getResources()).thenReturn(mockResources)
whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources)
whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width))
@@ -164,6 +166,60 @@ class DragPositioningCallbackUtilityTest {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+ fun testChangeBounds_unresizeableApp_heightLessThanMin_resetToStartingBounds() {
+ mockWindowDecoration.mTaskInfo.isResizeable = false
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Resize to width of 95px and height of 5px with min width of 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 5
+ val newY = STARTING_BOUNDS.top.toFloat() + 95
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ assertFalse(
+ DragPositioningCallbackUtility.changeBounds(
+ CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration
+ )
+ )
+
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+ fun testChangeBounds_unresizeableApp_widthLessThanMin_resetToStartingBounds() {
+ mockWindowDecoration.mTaskInfo.isResizeable = false
+ val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Resize to height of 95px and width of 5px with min width of 10px
+ val newX = STARTING_BOUNDS.right.toFloat() - 95
+ val newY = STARTING_BOUNDS.top.toFloat() + 5
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ assertFalse(
+ DragPositioningCallbackUtility.changeBounds(
+ CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration
+ )
+ )
+
+
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+
+ @Test
fun testChangeBoundsDoesNotChangeHeightWhenNegative() {
val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
val repositionTaskBounds = Rect(STARTING_BOUNDS)
@@ -317,6 +373,34 @@ class DragPositioningCallbackUtilityTest {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+ fun testChangeBounds_unresizeableApp_beyondStableBounds_resetToStartingBounds() {
+ mockWindowDecoration.mTaskInfo.isResizeable = false
+ val startingPoint = PointF(
+ STARTING_BOUNDS.right.toFloat(),
+ STARTING_BOUNDS.bottom.toFloat()
+ )
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+
+ // Resize to beyond stable bounds.
+ val newX = STARTING_BOUNDS.right.toFloat() + STABLE_BOUNDS.width()
+ val newY = STARTING_BOUNDS.bottom.toFloat() + STABLE_BOUNDS.height()
+
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+ assertFalse(
+ DragPositioningCallbackUtility.changeBounds(
+ CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration
+ )
+ )
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeLessThanMin_shouldNotChangeBounds() {
doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt
new file mode 100644
index 000000000000..ce17c1df50bc
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecoratorTests.kt
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor
+
+import android.app.ActivityManager
+import android.graphics.PointF
+import android.graphics.Rect
+import android.util.MathUtils.abs
+import android.util.MathUtils.max
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CtrlType
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import kotlin.math.min
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.never
+
+/**
+ * Tests for the [FixedAspectRatioTaskPositionerDecorator], written in parameterized form to check
+ * decorators behaviour for different variations of drag actions.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:FixedAspectRatioTaskPositionerDecoratorTests
+ */
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class FixedAspectRatioTaskPositionerDecoratorTests : ShellTestCase(){
+ @Mock
+ private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration
+ @Mock
+ private lateinit var mockTaskPositioner: VeiledResizeTaskPositioner
+
+ private lateinit var decoratedTaskPositioner: FixedAspectRatioTaskPositionerDecorator
+
+ @Before
+ fun setUp() {
+ mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ isResizeable = false
+ configuration.windowConfiguration.setBounds(PORTRAIT_BOUNDS)
+ }
+ doReturn(PORTRAIT_BOUNDS).`when`(mockTaskPositioner).onDragPositioningStart(
+ any(), any(), any())
+ doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningMove(any(), any())
+ doReturn(Rect()).`when`(mockTaskPositioner).onDragPositioningEnd(any(), any())
+ decoratedTaskPositioner = spy(
+ FixedAspectRatioTaskPositionerDecorator(
+ mockDesktopWindowDecoration, mockTaskPositioner)
+ )
+ }
+
+ @Test
+ fun testOnDragPositioningStart_noAdjustment(
+ @TestParameter testCase: ResizeableOrNotResizingTestCases
+ ) {
+ val originalX = 0f
+ val originalY = 0f
+ mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ isResizeable = testCase.isResizeable
+ }
+
+ decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY)
+
+ val capturedValues = getLatestOnStartArguments()
+ assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType)
+ assertThat(capturedValues.x).isEqualTo(originalX)
+ assertThat(capturedValues.y).isEqualTo(originalY)
+ }
+
+ @Test
+ fun testOnDragPositioningStart_cornerResize_noAdjustment(
+ @TestParameter testCase: CornerResizeStartTestCases
+ ) {
+ val originalX = 0f
+ val originalY = 0f
+
+ decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalY)
+
+ val capturedValues = getLatestOnStartArguments()
+ assertThat(capturedValues.ctrlType).isEqualTo(testCase.ctrlType)
+ assertThat(capturedValues.x).isEqualTo(originalX)
+ assertThat(capturedValues.y).isEqualTo(originalY)
+ }
+
+ @Test
+ fun testOnDragPositioningStart_edgeResize_ctrlTypeAdjusted(
+ @TestParameter testCase: EdgeResizeStartTestCases, @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getEdgeStartingPoint(
+ testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ val adjustedCtrlType = testCase.ctrlType + testCase.additionalEdgeCtrlType
+ val capturedValues = getLatestOnStartArguments()
+ assertThat(capturedValues.ctrlType).isEqualTo(adjustedCtrlType)
+ assertThat(capturedValues.x).isEqualTo(startingPoint.x)
+ assertThat(capturedValues.y).isEqualTo(startingPoint.y)
+ }
+
+ @Test
+ fun testOnDragPositioningMove_noAdjustment(
+ @TestParameter testCase: ResizeableOrNotResizingTestCases
+ ) {
+ val originalX = 0f
+ val originalY = 0f
+ decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX)
+ mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ isResizeable = testCase.isResizeable
+ }
+
+ decoratedTaskPositioner.onDragPositioningMove(
+ originalX + SMALL_DELTA, originalY + SMALL_DELTA)
+
+ val capturedValues = getLatestOnMoveArguments()
+ assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA)
+ assertThat(capturedValues.y).isEqualTo(originalY + SMALL_DELTA)
+ }
+
+ @Test
+ fun testOnDragPositioningMove_cornerResize_invalidRegion_noResize(
+ @TestParameter testCase: InvalidCornerResizeTestCases,
+ @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ val updatedBounds = decoratedTaskPositioner.onDragPositioningMove(
+ startingPoint.x + testCase.dragDelta.x,
+ startingPoint.y + testCase.dragDelta.y)
+
+ verify(mockTaskPositioner, never()).onDragPositioningMove(any(), any())
+ assertThat(updatedBounds).isEqualTo(startingBounds)
+ }
+
+
+ @Test
+ fun testOnDragPositioningMove_cornerResize_validRegion_resizeToAdjustedCoordinates(
+ @TestParameter testCase: ValidCornerResizeTestCases,
+ @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ decoratedTaskPositioner.onDragPositioningMove(
+ startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y)
+
+ val adjustedDragDelta = calculateAdjustedDelta(
+ testCase.ctrlType, testCase.dragDelta, orientation)
+ val capturedValues = getLatestOnMoveArguments()
+ val absChangeX = abs(capturedValues.x - startingPoint.x)
+ val absChangeY = abs(capturedValues.y - startingPoint.y)
+ val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY)
+ assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x)
+ assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y)
+ assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO)
+ }
+
+ @Test
+ fun testOnDragPositioningMove_edgeResize_resizeToAdjustedCoordinates(
+ @TestParameter testCase: EdgeResizeTestCases,
+ @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getEdgeStartingPoint(
+ testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ decoratedTaskPositioner.onDragPositioningMove(
+ startingPoint.x + testCase.dragDelta.x,
+ startingPoint.y + testCase.dragDelta.y)
+
+ val adjustedDragDelta = calculateAdjustedDelta(
+ testCase.ctrlType + testCase.additionalEdgeCtrlType,
+ testCase.dragDelta,
+ orientation)
+ val capturedValues = getLatestOnMoveArguments()
+ val absChangeX = abs(capturedValues.x - startingPoint.x)
+ val absChangeY = abs(capturedValues.y - startingPoint.y)
+ val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY)
+ assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x)
+ assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y)
+ assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO)
+ }
+
+ @Test
+ fun testOnDragPositioningEnd_noAdjustment(
+ @TestParameter testCase: ResizeableOrNotResizingTestCases
+ ) {
+ val originalX = 0f
+ val originalY = 0f
+ decoratedTaskPositioner.onDragPositioningStart(testCase.ctrlType, originalX, originalX)
+ mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ isResizeable = testCase.isResizeable
+ }
+
+ decoratedTaskPositioner.onDragPositioningEnd(
+ originalX + SMALL_DELTA, originalY + SMALL_DELTA)
+
+ val capturedValues = getLatestOnEndArguments()
+ assertThat(capturedValues.x).isEqualTo(originalX + SMALL_DELTA)
+ assertThat(capturedValues.y).isEqualTo(originalY + SMALL_DELTA)
+ }
+
+ @Test
+ fun testOnDragPositioningEnd_cornerResize_invalidRegion_endsAtPreviousValidPoint(
+ @TestParameter testCase: InvalidCornerResizeTestCases,
+ @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ decoratedTaskPositioner.onDragPositioningEnd(
+ startingPoint.x + testCase.dragDelta.x,
+ startingPoint.y + testCase.dragDelta.y)
+
+ val capturedValues = getLatestOnEndArguments()
+ assertThat(capturedValues.x).isEqualTo(startingPoint.x)
+ assertThat(capturedValues.y).isEqualTo(startingPoint.y)
+ }
+
+ @Test
+ fun testOnDragPositioningEnd_cornerResize_validRegion_endAtAdjustedCoordinates(
+ @TestParameter testCase: ValidCornerResizeTestCases,
+ @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getCornerStartingPoint(testCase.ctrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ decoratedTaskPositioner.onDragPositioningEnd(
+ startingPoint.x + testCase.dragDelta.x, startingPoint.y + testCase.dragDelta.y)
+
+ val adjustedDragDelta = calculateAdjustedDelta(
+ testCase.ctrlType, testCase.dragDelta, orientation)
+ val capturedValues = getLatestOnEndArguments()
+ val absChangeX = abs(capturedValues.x - startingPoint.x)
+ val absChangeY = abs(capturedValues.y - startingPoint.y)
+ val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY)
+ assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x)
+ assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y)
+ assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO)
+ }
+
+ @Test
+ fun testOnDragPositioningEnd_edgeResize_endAtAdjustedCoordinates(
+ @TestParameter testCase: EdgeResizeTestCases,
+ @TestParameter orientation: Orientation
+ ) {
+ val startingBounds = getAndMockBounds(orientation)
+ val startingPoint = getEdgeStartingPoint(
+ testCase.ctrlType, testCase.additionalEdgeCtrlType, startingBounds)
+
+ decoratedTaskPositioner.onDragPositioningStart(
+ testCase.ctrlType, startingPoint.x, startingPoint.y)
+
+ decoratedTaskPositioner.onDragPositioningEnd(
+ startingPoint.x + testCase.dragDelta.x,
+ startingPoint.y + testCase.dragDelta.y)
+
+ val adjustedDragDelta = calculateAdjustedDelta(
+ testCase.ctrlType + testCase.additionalEdgeCtrlType,
+ testCase.dragDelta,
+ orientation)
+ val capturedValues = getLatestOnEndArguments()
+ val absChangeX = abs(capturedValues.x - startingPoint.x)
+ val absChangeY = abs(capturedValues.y - startingPoint.y)
+ val resultAspectRatio = max(absChangeX, absChangeY) / min(absChangeX, absChangeY)
+ assertThat(capturedValues.x).isEqualTo(startingPoint.x + adjustedDragDelta.x)
+ assertThat(capturedValues.y).isEqualTo(startingPoint.y + adjustedDragDelta.y)
+ assertThat(resultAspectRatio).isEqualTo(STARTING_ASPECT_RATIO)
+ }
+
+ /**
+ * Returns the most recent arguments passed to the `.onPositioningStart()` of the
+ * [mockTaskPositioner].
+ */
+ private fun getLatestOnStartArguments(): CtrlCoordinateCapture {
+ val captorCtrlType = argumentCaptor<Int>()
+ val captorCoordinates = argumentCaptor<Float>()
+ verify(mockTaskPositioner).onDragPositioningStart(
+ captorCtrlType.capture(), captorCoordinates.capture(), captorCoordinates.capture())
+
+ return CtrlCoordinateCapture(captorCtrlType.firstValue, captorCoordinates.firstValue,
+ captorCoordinates.secondValue)
+ }
+
+ /**
+ * Returns the most recent arguments passed to the `.onPositioningMove()` of the
+ * [mockTaskPositioner].
+ */
+ private fun getLatestOnMoveArguments(): PointF {
+ val captorCoordinates = argumentCaptor<Float>()
+ verify(mockTaskPositioner).onDragPositioningMove(
+ captorCoordinates.capture(), captorCoordinates.capture())
+
+ return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue)
+ }
+
+ /**
+ * Returns the most recent arguments passed to the `.onPositioningEnd()` of the
+ * [mockTaskPositioner].
+ */
+ private fun getLatestOnEndArguments(): PointF {
+ val captorCoordinates = argumentCaptor<Float>()
+ verify(mockTaskPositioner).onDragPositioningEnd(
+ captorCoordinates.capture(), captorCoordinates.capture())
+
+ return PointF(captorCoordinates.firstValue, captorCoordinates.secondValue)
+ }
+
+ /**
+ * Mocks the app bounds to correspond with a given orientation and returns the mocked bounds.
+ */
+ private fun getAndMockBounds(orientation: Orientation): Rect {
+ val mockBounds = if (orientation.isPortrait) PORTRAIT_BOUNDS else LANDSCAPE_BOUNDS
+ doReturn(mockBounds).`when`(mockTaskPositioner).onDragPositioningStart(
+ any(), any(), any())
+ doReturn(mockBounds).`when`(decoratedTaskPositioner).getBounds(any())
+ return mockBounds
+ }
+
+ /**
+ * Calculates the corner point a given drag action should start from, based on the [ctrlType],
+ * given the [startingBounds].
+ */
+ private fun getCornerStartingPoint(@CtrlType ctrlType: Int, startingBounds: Rect): PointF {
+ return when (ctrlType) {
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT ->
+ PointF(startingBounds.right.toFloat(), startingBounds.bottom.toFloat())
+
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT ->
+ PointF(startingBounds.left.toFloat(), startingBounds.bottom.toFloat())
+
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT ->
+ PointF(startingBounds.right.toFloat(), startingBounds.top.toFloat())
+ // CTRL_TYPE_TOP + CTRL_TYPE_LEFT
+ else ->
+ PointF(startingBounds.left.toFloat(), startingBounds.top.toFloat())
+ }
+ }
+
+ /**
+ * Calculates the point along an edge the edge resize should start from, based on the starting
+ * edge ([edgeCtrlType]) and the additional edge we expect to resize ([additionalEdgeCtrlType]),
+ * given the [startingBounds].
+ */
+ private fun getEdgeStartingPoint(
+ @CtrlType edgeCtrlType: Int, @CtrlType additionalEdgeCtrlType: Int, startingBounds: Rect
+ ): PointF {
+ val simulatedCorner = getCornerStartingPoint(
+ edgeCtrlType + additionalEdgeCtrlType, startingBounds)
+ when (additionalEdgeCtrlType) {
+ CTRL_TYPE_TOP -> {
+ simulatedCorner.offset(0f, -SMALL_DELTA)
+ return simulatedCorner
+ }
+ CTRL_TYPE_BOTTOM -> {
+ simulatedCorner.offset(0f, SMALL_DELTA)
+ return simulatedCorner
+ }
+ CTRL_TYPE_LEFT -> {
+ simulatedCorner.offset(SMALL_DELTA, 0f)
+ return simulatedCorner
+ }
+ // CTRL_TYPE_RIGHT
+ else -> {
+ simulatedCorner.offset(-SMALL_DELTA, 0f)
+ return simulatedCorner
+ }
+ }
+ }
+
+ /**
+ * Calculates the adjustments to the drag delta we expect for a given action and orientation.
+ */
+ private fun calculateAdjustedDelta(
+ @CtrlType ctrlType: Int, delta: PointF, orientation: Orientation
+ ): PointF {
+ if ((abs(delta.x) < abs(delta.y) && delta.x != 0f) || delta.y == 0f) {
+ // Only respect x delta if it's less than y delta but non-zero (i.e there is a change
+ // in x to be applied), or if the y delta is zero (i.e there is no change in y to be
+ // applied).
+ val adjustedY = if (orientation.isPortrait)
+ delta.x * STARTING_ASPECT_RATIO else
+ delta.x / STARTING_ASPECT_RATIO
+ if (ctrlType.isBottomRightOrTopLeftCorner()) {
+ return PointF(delta.x, adjustedY)
+ }
+ return PointF(delta.x, -adjustedY)
+ }
+ // Respect y delta.
+ val adjustedX = if (orientation.isPortrait)
+ delta.y / STARTING_ASPECT_RATIO else
+ delta.y * STARTING_ASPECT_RATIO
+ if (ctrlType.isBottomRightOrTopLeftCorner()) {
+ return PointF(adjustedX, delta.y)
+ }
+ return PointF(-adjustedX, delta.y)
+ }
+
+ private fun @receiver:CtrlType Int.isBottomRightOrTopLeftCorner(): Boolean {
+ return this == CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT || this == CTRL_TYPE_TOP + CTRL_TYPE_LEFT
+ }
+
+ private inner class CtrlCoordinateCapture(ctrl: Int, xValue: Float, yValue: Float) {
+ var ctrlType = ctrl
+ var x = xValue
+ var y = yValue
+ }
+
+ companion object {
+ private val PORTRAIT_BOUNDS = Rect(100, 100, 200, 400)
+ private val LANDSCAPE_BOUNDS = Rect(100, 100, 400, 200)
+ private val STARTING_ASPECT_RATIO = PORTRAIT_BOUNDS.height() / PORTRAIT_BOUNDS.width()
+ private const val LARGE_DELTA = 50f
+ private const val SMALL_DELTA = 30f
+
+ enum class Orientation(
+ val isPortrait: Boolean
+ ) {
+ PORTRAIT (true),
+ LANDSCAPE (false)
+ }
+
+ enum class ResizeableOrNotResizingTestCases(
+ val ctrlType: Int,
+ val isResizeable: Boolean
+ ) {
+ NotResizing (CTRL_TYPE_UNDEFINED, false),
+ Resizeable (CTRL_TYPE_RIGHT, true)
+ }
+
+ /**
+ * Tests cases for the start of a corner resize.
+ * @param ctrlType the control type of the corner the resize is initiated on.
+ */
+ enum class CornerResizeStartTestCases(
+ val ctrlType: Int
+ ) {
+ BottomRightCorner (CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT),
+ BottomLeftCorner (CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT),
+ TopRightCorner (CTRL_TYPE_TOP + CTRL_TYPE_RIGHT),
+ TopLeftCorner (CTRL_TYPE_TOP + CTRL_TYPE_LEFT)
+ }
+
+ /**
+ * Tests cases for the moving and ending of a invalid corner resize. Where the compass point
+ * (e.g `SouthEast`) represents the direction of the drag.
+ * @param ctrlType the control type of the corner the resize is initiated on.
+ * @param dragDelta the delta of the attempted drag action, from the [ctrlType]'s
+ * corresponding corner point. Represented as a combination a different signed small and
+ * large deltas which correspond to the direction/angle of drag.
+ */
+ enum class InvalidCornerResizeTestCases(
+ val ctrlType: Int,
+ val dragDelta: PointF
+ ) {
+ BottomRightCornerNorthEastDrag (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT,
+ PointF(LARGE_DELTA, -LARGE_DELTA)),
+ BottomRightCornerSouthWestDrag (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT,
+ PointF(-LARGE_DELTA, LARGE_DELTA)),
+ TopLeftCornerNorthEastDrag (
+ CTRL_TYPE_TOP + CTRL_TYPE_LEFT,
+ PointF(LARGE_DELTA, -LARGE_DELTA)),
+ TopLeftCornerSouthWestDrag (
+ CTRL_TYPE_TOP + CTRL_TYPE_LEFT,
+ PointF(-LARGE_DELTA, LARGE_DELTA)),
+ BottomLeftCornerSouthEastDrag (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT,
+ PointF(LARGE_DELTA, LARGE_DELTA)),
+ BottomLeftCornerNorthWestDrag (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT,
+ PointF(-LARGE_DELTA, -LARGE_DELTA)),
+ TopRightCornerSouthEastDrag (
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT,
+ PointF(LARGE_DELTA, LARGE_DELTA)),
+ TopRightCornerNorthWestDrag (
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT,
+ PointF(-LARGE_DELTA, -LARGE_DELTA)),
+ }
+
+ /**
+ * Tests cases for the moving and ending of a valid corner resize. Where the compass point
+ * (e.g `SouthEast`) represents the direction of the drag, followed by the expected
+ * behaviour in that direction (i.e `RespectY` means the y delta will be respected whereas
+ * `RespectX` means the x delta will be respected).
+ * @param ctrlType the control type of the corner the resize is initiated on.
+ * @param dragDelta the delta of the attempted drag action, from the [ctrlType]'s
+ * corresponding corner point. Represented as a combination a different signed small and
+ * large deltas which correspond to the direction/angle of drag.
+ */
+ enum class ValidCornerResizeTestCases(
+ val ctrlType: Int,
+ val dragDelta: PointF,
+ ) {
+ BottomRightCornerSouthEastDragRespectY (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT,
+ PointF(+LARGE_DELTA, SMALL_DELTA)),
+ BottomRightCornerSouthEastDragRespectX (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT,
+ PointF(SMALL_DELTA, LARGE_DELTA)),
+ BottomRightCornerNorthWestDragRespectY (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT,
+ PointF(-LARGE_DELTA, -SMALL_DELTA)),
+ BottomRightCornerNorthWestDragRespectX (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_RIGHT,
+ PointF(-SMALL_DELTA, -LARGE_DELTA)),
+ TopLeftCornerSouthEastDragRespectY (
+ CTRL_TYPE_TOP + CTRL_TYPE_LEFT,
+ PointF(LARGE_DELTA, SMALL_DELTA)),
+ TopLeftCornerSouthEastDragRespectX (
+ CTRL_TYPE_TOP + CTRL_TYPE_LEFT,
+ PointF(SMALL_DELTA, LARGE_DELTA)),
+ TopLeftCornerNorthWestDragRespectY (
+ CTRL_TYPE_TOP + CTRL_TYPE_LEFT,
+ PointF(-LARGE_DELTA, -SMALL_DELTA)),
+ TopLeftCornerNorthWestDragRespectX (
+ CTRL_TYPE_TOP + CTRL_TYPE_LEFT,
+ PointF(-SMALL_DELTA, -LARGE_DELTA)),
+ BottomLeftCornerSouthWestDragRespectY (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT,
+ PointF(-LARGE_DELTA, SMALL_DELTA)),
+ BottomLeftCornerSouthWestDragRespectX (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT,
+ PointF(-SMALL_DELTA, LARGE_DELTA)),
+ BottomLeftCornerNorthEastDragRespectY (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT,
+ PointF(LARGE_DELTA, -SMALL_DELTA)),
+ BottomLeftCornerNorthEastDragRespectX (
+ CTRL_TYPE_BOTTOM + CTRL_TYPE_LEFT,
+ PointF(SMALL_DELTA, -LARGE_DELTA)),
+ TopRightCornerSouthWestDragRespectY (
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT,
+ PointF(-LARGE_DELTA, SMALL_DELTA)),
+ TopRightCornerSouthWestDragRespectX (
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT,
+ PointF(-SMALL_DELTA, LARGE_DELTA)),
+ TopRightCornerNorthEastDragRespectY (
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT,
+ PointF(LARGE_DELTA, -SMALL_DELTA)),
+ TopRightCornerNorthEastDragRespectX (
+ CTRL_TYPE_TOP + CTRL_TYPE_RIGHT,
+ PointF(+SMALL_DELTA, -LARGE_DELTA))
+ }
+
+ /**
+ * Tests cases for the start of an edge resize.
+ * @param ctrlType the control type of the edge the resize is initiated on.
+ * @param additionalEdgeCtrlType the expected additional edge to be included in the ctrl
+ * type.
+ */
+ enum class EdgeResizeStartTestCases(
+ val ctrlType: Int,
+ val additionalEdgeCtrlType: Int
+ ) {
+ BottomOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_BOTTOM),
+ TopOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_TOP),
+ BottomOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_BOTTOM),
+ TopOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_TOP),
+ RightOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_RIGHT),
+ LeftOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_LEFT),
+ RightOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_RIGHT),
+ LeftOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_LEFT)
+ }
+
+ /**
+ * Tests cases for the moving and ending of an edge resize.
+ * @param ctrlType the control type of the edge the resize is initiated on.
+ * @param additionalEdgeCtrlType the expected additional edge to be included in the ctrl
+ * type.
+ * @param dragDelta the delta of the attempted drag action, from the [ctrlType]'s
+ * corresponding edge point. Represented as a combination a different signed small and
+ * large deltas which correspond to the direction/angle of drag.
+ */
+ enum class EdgeResizeTestCases(
+ val ctrlType: Int,
+ val additionalEdgeCtrlType: Int,
+ val dragDelta: PointF
+ ) {
+ BottomOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_BOTTOM, PointF(-SMALL_DELTA, 0f)),
+ TopOfLeftEdgeResize (CTRL_TYPE_LEFT, CTRL_TYPE_TOP, PointF(-SMALL_DELTA, 0f)),
+ BottomOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_BOTTOM, PointF(SMALL_DELTA, 0f)),
+ TopOfRightEdgeResize (CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, PointF(SMALL_DELTA, 0f)),
+ RightOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_RIGHT, PointF(0f, -SMALL_DELTA)),
+ LeftOfTopEdgeResize (CTRL_TYPE_TOP, CTRL_TYPE_LEFT, PointF(0f, -SMALL_DELTA)),
+ RightOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_RIGHT, PointF(0f, SMALL_DELTA)),
+ LeftOfBottomEdgeResize (CTRL_TYPE_BOTTOM, CTRL_TYPE_LEFT, PointF(0f, SMALL_DELTA))
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 3a3e965b625e..7543fed4b085 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -121,6 +121,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
displayId = DISPLAY_ID
configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
configuration.windowConfiguration.displayRotation = ROTATION_90
+ isResizeable = true
}
`when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA)
mockWindowDecoration.mDisplay = mockDisplay
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
index a84523112d9b..cabd472ec263 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -134,7 +134,7 @@ class HandleMenuTest : ShellTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
fun testFullscreenMenuUsesSystemViewContainer() {
createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED)
val handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED)
@@ -146,7 +146,7 @@ class HandleMenuTest : ShellTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
fun testFreeformMenu_usesViewHostViewContainer() {
createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED)
handleMenu = createAndShowHandleMenu(SPLIT_POSITION_UNDEFINED)
@@ -157,7 +157,7 @@ class HandleMenuTest : ShellTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
fun testSplitLeftMenu_usesSystemViewContainer() {
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT)
handleMenu = createAndShowHandleMenu(SPLIT_POSITION_TOP_OR_LEFT)
@@ -172,7 +172,7 @@ class HandleMenuTest : ShellTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ @EnableFlags(Flags.FLAG_ENABLE_HANDLE_INPUT_FIX)
fun testSplitRightMenu_usesSystemViewContainer() {
createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT)
handleMenu = createAndShowHandleMenu(SPLIT_POSITION_BOTTOM_OR_RIGHT)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 6ae16edaf3df..7784af6b1111 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -141,6 +141,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
displayId = DISPLAY_ID
configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
configuration.windowConfiguration.displayRotation = ROTATION_90
+ isResizeable = true
}
`when`(mockDesktopWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA)
mockDesktopWindowDecoration.mDisplay = mockDisplay
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
index 1b2ce9e4df36..1b0b7d95e657 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/DefaultWindowDecorViewHostTest.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.windowdecor.viewhost
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.SurfaceControl
-import android.view.SurfaceControlViewHost
import android.view.View
import android.view.WindowManager
import androidx.test.filters.SmallTest
@@ -28,7 +27,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
@@ -59,54 +57,8 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
onDrawTransaction = null
)
- assertThat(windowDecorViewHost.viewHost).isNotNull()
- assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
- }
-
- @Test
- fun updateView_alreadyLaidOut_relayouts() = runTest {
- val windowDecorViewHost = createDefaultViewHost()
- val view = View(context)
- windowDecorViewHost.updateView(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100),
- configuration = context.resources.configuration,
- onDrawTransaction = null
- )
-
- val otherParams = WindowManager.LayoutParams(200, 200)
- windowDecorViewHost.updateView(
- view = view,
- attrs = otherParams,
- configuration = context.resources.configuration,
- onDrawTransaction = null
- )
-
- assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(view)
- assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
- .isEqualTo(otherParams.width)
- }
-
- @Test
- fun updateView_replacingView_throws() = runTest {
- val windowDecorViewHost = createDefaultViewHost()
- val view = View(context)
- windowDecorViewHost.updateView(
- view = view,
- attrs = WindowManager.LayoutParams(100, 100),
- configuration = context.resources.configuration,
- onDrawTransaction = null
- )
-
- val otherView = View(context)
- assertThrows(Exception::class.java) {
- windowDecorViewHost.updateView(
- view = otherView,
- attrs = WindowManager.LayoutParams(100, 100),
- configuration = context.resources.configuration,
- onDrawTransaction = null
- )
- }
+ assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(windowDecorViewHost.view()).isEqualTo(view)
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -125,7 +77,7 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
)
// No view host yet, since the coroutine hasn't run.
- assertThat(windowDecorViewHost.viewHost).isNull()
+ assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()
windowDecorViewHost.updateView(
view = syncView,
@@ -137,14 +89,13 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
// Would run coroutine if it hadn't been cancelled.
advanceUntilIdle()
- assertThat(windowDecorViewHost.viewHost).isNotNull()
- assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
+ assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(windowDecorViewHost.view()).isNotNull()
// View host view/attrs should match the ones from the sync call, plus, since the
// sync/async were made with different views, if the job hadn't been cancelled there
// would've been an exception thrown as replacing views isn't allowed.
- assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(syncView)
- assertThat(windowDecorViewHost.viewHost!!.view!!.layoutParams.width)
- .isEqualTo(syncAttrs.width)
+ assertThat(windowDecorViewHost.view()).isEqualTo(syncView)
+ assertThat(windowDecorViewHost.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -160,11 +111,11 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
configuration = context.resources.configuration,
)
- assertThat(windowDecorViewHost.viewHost).isNull()
+ assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isFalse()
advanceUntilIdle()
- assertThat(windowDecorViewHost.viewHost).isNotNull()
+ assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -187,9 +138,8 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
advanceUntilIdle()
- assertThat(windowDecorViewHost.viewHost).isNotNull()
- assertThat(windowDecorViewHost.viewHost!!.view).isNotNull()
- assertThat(windowDecorViewHost.viewHost!!.view).isEqualTo(otherView)
+ assertThat(windowDecorViewHost.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(windowDecorViewHost.view()).isEqualTo(otherView)
}
@Test
@@ -207,16 +157,15 @@ class DefaultWindowDecorViewHostTest : ShellTestCase() {
val t = mock(SurfaceControl.Transaction::class.java)
windowDecorViewHost.release(t)
- verify(windowDecorViewHost.viewHost!!).release()
- verify(t).remove(windowDecorViewHost.surfaceControl)
+ verify(windowDecorViewHost.viewHostAdapter).release(t)
}
private fun CoroutineScope.createDefaultViewHost() = DefaultWindowDecorViewHost(
context = context,
mainScope = this,
display = context.display,
- surfaceControlViewHostFactory = { c, d, wwm, s ->
- spy(SurfaceControlViewHost(c, d, wwm, s))
- }
+ viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
)
+
+ private fun DefaultWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt
new file mode 100644
index 000000000000..a7e4213ad01d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/PooledWindowDecorViewHostSupplierTest.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.sysui.ShellInit
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [PooledWindowDecorViewHostSupplier].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:PooledWindowDecorViewHostSupplierTest
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PooledWindowDecorViewHostSupplierTest : ShellTestCase() {
+
+ private val testExecutor = TestShellExecutor()
+ private val testShellInit = ShellInit(testExecutor)
+ @Mock
+ private lateinit var mockViewHostFactory: ReusableWindowDecorViewHost.Factory
+
+ private lateinit var supplier: PooledWindowDecorViewHostSupplier
+
+ @Test
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun onInit_warmsAndPoolsViewHosts() = runTest {
+ supplier = createSupplier(maxPoolSize = 5, preWarmSize = 2)
+ val mockViewHost1 = mock<ReusableWindowDecorViewHost>()
+ val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
+ whenever(mockViewHostFactory
+ .create(context, this, context.display, id = 0))
+ .thenReturn(mockViewHost1)
+ whenever(mockViewHostFactory
+ .create(context, this, context.display, id = 1))
+ .thenReturn(mockViewHost2)
+
+ testExecutor.flushAll()
+ advanceUntilIdle()
+
+ // Both were warmed up.
+ verify(mockViewHost1).warmUp()
+ verify(mockViewHost2).warmUp()
+ // Both were released, so re-acquiring them provides the same instance.
+ assertThat(mockViewHost2)
+ .isEqualTo(supplier.acquire(context, context.display))
+ assertThat(mockViewHost1)
+ .isEqualTo(supplier.acquire(context, context.display))
+ }
+
+ @Test(expected = Throwable::class)
+ fun onInit_warmUpSizeExceedsPoolSize_throws() = runTest {
+ createSupplier(maxPoolSize = 3, preWarmSize = 4)
+ }
+
+ @Test
+ fun acquire_poolHasInstances_reuses() = runTest {
+ supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
+
+ // Prepare the pool with one instance.
+ val mockViewHost = mock<ReusableWindowDecorViewHost>()
+ supplier.release(mockViewHost, SurfaceControl.Transaction())
+
+ assertThat(mockViewHost)
+ .isEqualTo(supplier.acquire(context, context.display))
+ verify(mockViewHostFactory, never()).create(any(), any(), any(), any())
+ }
+
+ @Test
+ fun acquire_pooledHasZeroInstances_creates() = runTest {
+ supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
+
+ supplier.acquire(context, context.display)
+
+ verify(mockViewHostFactory).create(context, this, context.display, id = 0)
+ }
+
+ @Test
+ fun release_poolBelowLimit_caches() = runTest {
+ supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
+
+ val mockViewHost = mock<ReusableWindowDecorViewHost>()
+ val mockT = mock<SurfaceControl.Transaction>()
+ supplier.release(mockViewHost, mockT)
+
+ assertThat(mockViewHost)
+ .isEqualTo(supplier.acquire(context, context.display))
+ }
+
+ @Test
+ fun release_poolBelowLimit_doesNotReleaseViewHost() = runTest {
+ supplier = createSupplier(maxPoolSize = 5, preWarmSize = 0)
+
+ val mockViewHost = mock<ReusableWindowDecorViewHost>()
+ val mockT = mock<SurfaceControl.Transaction>()
+ supplier.release(mockViewHost, mockT)
+
+ verify(mockViewHost, never()).release(mockT)
+ }
+
+ @Test
+ fun release_poolAtLimit_doesNotCache() = runTest {
+ supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0)
+ val mockT = mock<SurfaceControl.Transaction>()
+ val mockViewHost = mock<ReusableWindowDecorViewHost>()
+ supplier.release(mockViewHost, mockT) // Maxes pool.
+
+ val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
+ supplier.release(mockViewHost2, mockT) // Beyond limit.
+
+ assertThat(mockViewHost)
+ .isEqualTo(supplier.acquire(context, context.display))
+ // Second one wasn't cached, so the acquired one should've been a new instance.
+ assertThat(mockViewHost2)
+ .isNotEqualTo(supplier.acquire(context, context.display))
+ }
+
+ @Test
+ fun release_poolAtLimit_releasesViewHost() = runTest {
+ supplier = createSupplier(maxPoolSize = 1, preWarmSize = 0)
+ val mockT = mock<SurfaceControl.Transaction>()
+ val mockViewHost = mock<ReusableWindowDecorViewHost>()
+ supplier.release(mockViewHost, mockT) // Maxes pool.
+
+ val mockViewHost2 = mock<ReusableWindowDecorViewHost>()
+ supplier.release(mockViewHost2, mockT) // Beyond limit.
+
+ // Second one doesn't fit, so it needs to be released.
+ verify(mockViewHost2).release(mockT)
+ }
+
+ private fun CoroutineScope.createSupplier(
+ maxPoolSize: Int,
+ preWarmSize: Int
+ ) = PooledWindowDecorViewHostSupplier(
+ context,
+ this,
+ testShellInit,
+ mockViewHostFactory,
+ maxPoolSize,
+ preWarmSize
+ ).also {
+ testShellInit.init()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt
new file mode 100644
index 000000000000..de2444e34ca9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/ReusableWindowDecorViewHostTest.kt
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [ReusableWindowDecorViewHost].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ReusableWindowDecorViewHostTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class ReusableWindowDecorViewHostTest : ShellTestCase() {
+
+ @Test
+ fun warmUp_addsRootView() = runTest {
+ val reusableVH = createReusableViewHost().apply {
+ warmUp()
+ }
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView)
+ }
+
+ @Test
+ fun update_differentView_replacesView() = runTest {
+ val view = View(context)
+ val lp = WindowManager.LayoutParams()
+ val reusableVH = createReusableViewHost()
+ reusableVH.updateView(view, lp, context.resources.configuration, null)
+
+ assertThat(reusableVH.rootView.childCount).isEqualTo(1)
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view)
+
+ val newView = View(context)
+ val newLp = WindowManager.LayoutParams()
+ reusableVH.updateView(newView, newLp, context.resources.configuration, null)
+
+ assertThat(reusableVH.rootView.childCount).isEqualTo(1)
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateView_clearsPendingAsyncJob() = runTest {
+ val reusableVH = createReusableViewHost()
+ val asyncView = View(context)
+ val syncView = View(context)
+ val asyncAttrs = WindowManager.LayoutParams(100, 100)
+ val syncAttrs = WindowManager.LayoutParams(200, 200)
+
+ reusableVH.updateViewAsync(
+ view = asyncView,
+ attrs = asyncAttrs,
+ configuration = context.resources.configuration,
+ )
+
+ // No view host yet, since the coroutine hasn't run.
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
+
+ reusableVH.updateView(
+ view = syncView,
+ attrs = syncAttrs,
+ configuration = context.resources.configuration,
+ onDrawTransaction = null
+ )
+
+ // Would run coroutine if it hadn't been cancelled.
+ advanceUntilIdle()
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ // View host view/attrs should match the ones from the sync call, plus, since the
+ // sync/async were made with different views, if the job hadn't been cancelled there
+ // would've been an exception thrown as replacing views isn't allowed.
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView)
+ assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateViewAsync() = runTest {
+ val reusableVH = createReusableViewHost()
+ val view = View(context)
+ val attrs = WindowManager.LayoutParams(100, 100)
+
+ reusableVH.updateViewAsync(
+ view = view,
+ attrs = attrs,
+ configuration = context.resources.configuration,
+ )
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isFalse()
+
+ advanceUntilIdle()
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun updateViewAsync_clearsPendingAsyncJob() = runTest {
+ val reusableVH = createReusableViewHost()
+
+ val view = View(context)
+ reusableVH.updateViewAsync(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ )
+ val otherView = View(context)
+ reusableVH.updateViewAsync(
+ view = otherView,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ )
+
+ advanceUntilIdle()
+
+ assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue()
+ assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView)
+ }
+
+ @Test
+ fun release() = runTest {
+ val reusableVH = createReusableViewHost()
+
+ val view = View(context)
+ reusableVH.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100),
+ configuration = context.resources.configuration,
+ onDrawTransaction = null
+ )
+
+ val t = mock(SurfaceControl.Transaction::class.java)
+ reusableVH.release(t)
+
+ verify(reusableVH.viewHostAdapter).release(t)
+ }
+
+ private fun CoroutineScope.createReusableViewHost() = ReusableWindowDecorViewHost(
+ context = context,
+ mainScope = this,
+ display = context.display,
+ id = 1,
+ viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)),
+ )
+
+ private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt
new file mode 100644
index 000000000000..d6c80a7fffc1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/viewhost/SurfaceControlViewHostAdapterTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.viewhost
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [SurfaceControlViewHostAdapter].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:SurfaceControlViewHostAdapterTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class SurfaceControlViewHostAdapterTest : ShellTestCase() {
+
+ private lateinit var adapter: SurfaceControlViewHostAdapter
+
+ @Before
+ fun setUp() {
+ adapter = SurfaceControlViewHostAdapter(
+ context,
+ context.display,
+ surfaceControlViewHostFactory = { c, d, wwm, s ->
+ spy(SurfaceControlViewHost(c, d, wwm, s))
+ }
+ )
+ }
+
+ @Test
+ fun prepareViewHost() {
+ adapter.prepareViewHost(context.resources.configuration)
+
+ assertThat(adapter.viewHost).isNotNull()
+ }
+
+ @Test
+ fun prepareViewHost_alreadyCreated_skips() {
+ adapter.prepareViewHost(context.resources.configuration)
+
+ val viewHost = adapter.viewHost!!
+
+ adapter.prepareViewHost(context.resources.configuration)
+
+ assertThat(adapter.viewHost).isEqualTo(viewHost)
+ }
+
+ @Test
+ fun updateView_layoutInViewHost() {
+ val view = View(context)
+ adapter.prepareViewHost(context.resources.configuration)
+
+ adapter.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100)
+ )
+
+ assertThat(adapter.isInitialized()).isTrue()
+ assertThat(adapter.view()).isEqualTo(view)
+ }
+
+ @Test
+ fun updateView_alreadyLaidOut_relayouts() {
+ val view = View(context)
+ adapter.prepareViewHost(context.resources.configuration)
+ adapter.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100)
+ )
+
+ val otherParams = WindowManager.LayoutParams(200, 200)
+ adapter.updateView(
+ view = view,
+ attrs = otherParams
+ )
+
+ assertThat(adapter.view()).isEqualTo(view)
+ assertThat(adapter.view()!!.layoutParams.width).isEqualTo(otherParams.width)
+ }
+
+ @Test
+ fun updateView_replacingView_throws() {
+ val view = View(context)
+ adapter.prepareViewHost(context.resources.configuration)
+ adapter.updateView(
+ view = view,
+ attrs = WindowManager.LayoutParams(100, 100)
+ )
+
+ val otherView = View(context)
+ assertThrows(Exception::class.java) {
+ adapter.updateView(
+ view = otherView,
+ attrs = WindowManager.LayoutParams(100, 100)
+ )
+ }
+ }
+
+ @Test
+ fun release() {
+ adapter.prepareViewHost(context.resources.configuration)
+ adapter.updateView(
+ view = View(context),
+ attrs = WindowManager.LayoutParams(100, 100)
+ )
+
+ val mockT = mock(SurfaceControl.Transaction::class.java)
+ adapter.release(mockT)
+
+ verify(adapter.viewHost!!).release()
+ verify(mockT).remove(adapter.rootSurface)
+ }
+
+ private fun SurfaceControlViewHostAdapter.view(): View? = viewHost?.view
+}