diff options
| author | 2024-09-16 23:18:36 +0000 | |
|---|---|---|
| committer | 2024-09-16 23:18:36 +0000 | |
| commit | 66baab9a17e512fdfd65643e628c594690b36bbb (patch) | |
| tree | a35537b08bb85da97523a0257d0c0fe9303529e8 /libs | |
| parent | 26b45954c3b649cb9aec3bfd28143323bf0520b0 (diff) | |
| parent | d00ccc1124509d7e1cf59e6e59c0e47a5c03c4ca (diff) | |
Snap for 12373988 from d00ccc1124509d7e1cf59e6e59c0e47a5c03c4ca to 24Q4-release
Change-Id: I2723e2caf6c24e94afe40d8b981c4f5515b96e0b
Diffstat (limited to 'libs')
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 +} |