diff options
Diffstat (limited to 'libs')
70 files changed, 2205 insertions, 264 deletions
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 957d1b835ec2..bcb6c4f555f7 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -170,9 +170,9 @@ android_library { "res", ], static_libs: [ + "//frameworks/base/packages/SystemUI/aconfig:com_android_systemui_flags_lib", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:iconloader_base", - "//packages/apps/Car/SystemUI/aconfig:com_android_systemui_car_flags_lib", "PlatformAnimationLib", "WindowManager-Shell-lite-proto", "WindowManager-Shell-proto", diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 4f1cd9780f8b..9c15319585b7 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -32,7 +32,7 @@ <activity android:name=".desktopmode.DesktopWallpaperActivity" android:excludeFromRecents="true" - android:launchMode="singleInstance" + android:launchMode="singleInstancePerTask" android:showForAllUsers="true" android:theme="@style/DesktopWallpaperTheme" /> diff --git a/libs/WindowManager/Shell/aconfig/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp index 7f8f57b172ff..f8da7fa86cff 100644 --- a/libs/WindowManager/Shell/aconfig/Android.bp +++ b/libs/WindowManager/Shell/aconfig/Android.bp @@ -4,6 +4,7 @@ aconfig_declarations { container: "system", srcs: [ "multitasking.aconfig", + "automotive.aconfig", ], } diff --git a/libs/WindowManager/Shell/aconfig/automotive.aconfig b/libs/WindowManager/Shell/aconfig/automotive.aconfig new file mode 100644 index 000000000000..2f25aa460ec1 --- /dev/null +++ b/libs/WindowManager/Shell/aconfig/automotive.aconfig @@ -0,0 +1,11 @@ +# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto + +package: "com.android.wm.shell" +container: "system" + +flag { + name: "enable_auto_task_stack_controller" + namespace: "multitasking" + description: "Enables auto task stack controller to manage task stacks on automotive" + bug: "384082238" +} diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 688bf83224aa..b10b099c970b 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -13,13 +13,6 @@ flag { } flag { - name: "enable_split_contextual" - namespace: "multitasking" - description: "Enables invoking split contextually" - bug: "276361926" -} - -flag { name: "enable_taskbar_navbar_unification" namespace: "multitasking" description: "Enables taskbar / navbar unification" @@ -104,6 +97,13 @@ flag { } flag { + name: "enable_create_any_bubble" + namespace: "multitasking" + description: "Enable UI affordances to create bubbles via launcher app icons" + bug: "385220199" +} + +flag { name: "only_reuse_bubbled_task_when_launched_from_bubble" namespace: "multitasking" description: "Allow reusing bubbled tasks for new activities only when launching from bubbles" diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt index c62d2a06bad5..90ea7d35015e 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt @@ -34,7 +34,6 @@ import com.android.internal.protolog.ProtoLog import com.android.internal.statusbar.IStatusBarService import com.android.wm.shell.Flags import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.Bubbles.SysuiProxy import com.android.wm.shell.bubbles.properties.ProdBubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository @@ -44,6 +43,7 @@ import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.shared.TransactionPool import com.android.wm.shell.shared.bubbles.BubbleBarLocation diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index ab2e552c7a3d..a7eebd6159e4 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -36,10 +36,10 @@ import com.android.internal.protolog.ProtoLog import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.Flags import com.android.wm.shell.R -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.Bubbles.SysuiProxy import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt index 3043e2bcb0be..a83327bbadee 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt @@ -35,7 +35,6 @@ import com.android.internal.protolog.ProtoLog import com.android.internal.statusbar.IStatusBarService import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController @@ -44,6 +43,7 @@ import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.shared.TransactionPool import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt index bcaa63bfad36..750178678785 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt @@ -22,9 +22,9 @@ import android.content.res.Resources import android.view.LayoutInflater import com.android.internal.logging.testing.UiEventLoggerFake import com.android.wm.shell.R -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.BubbleViewInfoTask.BubbleViewInfo import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView +import com.android.wm.shell.common.TestShellExecutor import com.google.common.util.concurrent.MoreExecutors.directExecutor /** Helper to create a [Bubble] instance */ diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt index c45f6903c2e1..9e58b5be9d0d 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt @@ -36,7 +36,6 @@ import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.protolog.ProtoLog -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.BubbleExpandedViewManager import com.android.wm.shell.bubbles.BubbleLogger @@ -46,6 +45,7 @@ import com.android.wm.shell.bubbles.BubbleTaskView import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.FakeBubbleFactory +import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat @@ -290,11 +290,10 @@ class BubbleBarAnimationHelperTest { assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() - val bbevBottom = bbev.contentBottomOnScreen + bubblePositioner.insets.top activityScenario.onActivity { // notify that the IME top coordinate is greater than the bottom of the expanded view. // there's no overlap so it should not be clipped. - animationHelper.onImeTopChanged(bbevBottom * 2) + animationHelper.onImeTopChanged(bbev.contentBottomOnScreen * 2) } val outline = Outline() bbev.outlineProvider.getOutline(bbev, outline) diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt index bfc798bb9c79..fbbcff2dee92 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -33,7 +33,6 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.BubbleExpandedViewManager import com.android.wm.shell.bubbles.BubbleLogger @@ -44,6 +43,7 @@ import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.RegionSamplingProvider import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat +import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.shared.handles.RegionSamplingHelper import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index 9b1645e9534c..5c5dde7da351 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -36,7 +36,6 @@ import com.android.internal.protolog.ProtoLog import com.android.internal.statusbar.IStatusBarService import com.android.wm.shell.R import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.BubbleController import com.android.wm.shell.bubbles.BubbleData @@ -58,6 +57,7 @@ import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.common.TestShellExecutor import com.android.wm.shell.shared.TransactionPool import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import com.android.wm.shell.shared.bubbles.BubbleBarLocation diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/TestShellExecutor.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestShellExecutor.kt index ef8e71c2590b..6b549b42cdcd 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/TestShellExecutor.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestShellExecutor.kt @@ -14,9 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell - -import com.android.wm.shell.common.ShellExecutor +package com.android.wm.shell.common /** * Simple implementation of [ShellExecutor] that collects all runnables and executes them diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml index 7347fbad5f5d..fc8b29912955 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml @@ -38,7 +38,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/bubble_popup_text_margin" android:maxWidth="@dimen/bubble_popup_content_max_width" - android:maxLines="1" + android:maxLines="2" android:ellipsize="end" android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline" android:textColor="@androidprv:color/materialColorOnSurface" diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml index f0e1871168dd..1616707954f5 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml @@ -38,7 +38,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/bubble_popup_text_margin" android:maxWidth="@dimen/bubble_popup_content_max_width" - android:maxLines="1" + android:maxLines="2" android:ellipsize="end" android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline" android:textColor="@androidprv:color/materialColorOnSurface" diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 6f18eda13caf..cf9c18b43a5e 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -23,7 +23,7 @@ <string name="pip_menu_title" msgid="5393619322111827096">"תפריט"</string> <string name="pip_menu_accessibility_title" msgid="8129016817688656249">"תפריט \'תמונה בתוך תמונה\'"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> במצב תמונה בתוך תמונה"</string> - <string name="pip_notification_message" msgid="8854051911700302620">"אם אינך רוצה שהתכונה הזו תשמש את <xliff:g id="NAME">%s</xliff:g>, יש להקיש כדי לפתוח את ההגדרות ולהשבית את התכונה."</string> + <string name="pip_notification_message" msgid="8854051911700302620">"אם אינך רוצה שהתכונה הזו תשמש את <xliff:g id="NAME">%s</xliff:g>, יש ללחוץ כדי לפתוח את ההגדרות ולהשבית את התכונה."</string> <string name="pip_play" msgid="3496151081459417097">"הפעלה"</string> <string name="pip_pause" msgid="690688849510295232">"השהיה"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"אפשר לדלג אל הבא"</string> @@ -53,7 +53,7 @@ <string name="accessibility_split_top" msgid="2789329702027147146">"פיצול למעלה"</string> <string name="accessibility_split_bottom" msgid="8694551025220868191">"פיצול למטה"</string> <string name="one_handed_tutorial_title" msgid="4583241688067426350">"איך להשתמש בתכונה \'מצב שימוש ביד אחת\'"</string> - <string name="one_handed_tutorial_description" msgid="3486582858591353067">"כדי לצאת, יש להחליק למעלה מתחתית המסך או להקיש במקום כלשהו במסך מעל האפליקציה"</string> + <string name="one_handed_tutorial_description" msgid="3486582858591353067">"כדי לצאת, יש להחליק למעלה מתחתית המסך או ללחוץ במקום כלשהו במסך מעל האפליקציה"</string> <string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"הפעלה של מצב שימוש ביד אחת"</string> <string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"יציאה ממצב שימוש ביד אחת"</string> <string name="bubbles_settings_button_description" msgid="1301286017420516912">"הגדרות לבועות של <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> @@ -76,33 +76,33 @@ <string name="bubble_fullscreen_text" msgid="1006758103218086231">"הצגה במסך מלא"</string> <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"אין להציג בועות לשיחה"</string> <string name="bubbles_user_education_title" msgid="2112319053732691899">"לדבר בבועות"</string> - <string name="bubbles_user_education_description" msgid="4215862563054175407">"שיחות חדשות מופיעות כסמלים צפים, או בועות. יש להקיש כדי לפתוח בועה. יש לגרור כדי להזיז אותה."</string> + <string name="bubbles_user_education_description" msgid="4215862563054175407">"שיחות חדשות מופיעות כסמלים צפים, או בועות. יש ללחוץ כדי לפתוח בועה. יש לגרור כדי להזיז אותה."</string> <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"שליטה בבועות, בכל זמן"</string> - <string name="bubbles_user_education_manage" msgid="3460756219946517198">"יש להקיש על \'ניהול\' כדי להשבית את הבועות מהאפליקציה הזו"</string> + <string name="bubbles_user_education_manage" msgid="3460756219946517198">"יש ללחוץ על \'ניהול\' כדי להשבית את הבועות מהאפליקציה הזו"</string> <string name="bubbles_user_education_got_it" msgid="3382046149225428296">"הבנתי"</string> <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"אין בועות מהזמן האחרון"</string> <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"בועות אחרונות ובועות שנסגרו יופיעו כאן"</string> <string name="bubble_bar_education_stack_title" msgid="2486903590422497245">"צ\'אט בבועות"</string> - <string name="bubble_bar_education_stack_text" msgid="2446934610817409820">"שיחות חדשות מופיעות כסמלים בפינה התחתונה של המסך. אפשר להקיש כדי להרחיב אותן או לגרור כדי לסגור אותן."</string> + <string name="bubble_bar_education_stack_text" msgid="2446934610817409820">"שיחות חדשות מופיעות כסמלים בפינה התחתונה של המסך. אפשר ללחוץ כדי להרחיב אותן או לגרור כדי לסגור אותן."</string> <string name="bubble_bar_education_manage_title" msgid="6148404487810835924">"שליטה בבועות בכל זמן"</string> - <string name="bubble_bar_education_manage_text" msgid="3199732148641842038">"אפשר להקיש כאן כדי לקבוע אילו אפליקציות ושיחות יוכלו להופיע בבועות"</string> + <string name="bubble_bar_education_manage_text" msgid="3199732148641842038">"אפשר ללחוץ כאן כדי לקבוע אילו אפליקציות ושיחות יוכלו להופיע בבועות"</string> <string name="notification_bubble_title" msgid="6082910224488253378">"בועה"</string> <string name="manage_bubbles_text" msgid="7730624269650594419">"ניהול"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"הבועה נסגרה."</string> <string name="bubble_shortcut_label" msgid="666269077944378311">"בועות"</string> <string name="bubble_shortcut_long_label" msgid="6088437544312894043">"הצגת הבועות"</string> - <string name="restart_button_description" msgid="4564728020654658478">"כדי לראות טוב יותר יש להקיש ולהפעיל את האפליקציה הזו מחדש"</string> + <string name="restart_button_description" msgid="4564728020654658478">"כדי לראות טוב יותר יש ללחוץ ולהפעיל את האפליקציה הזו מחדש"</string> <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"אפשר לשנות את יחס הגובה-רוחב של האפליקציה הזו ב\'הגדרות\'"</string> <string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"שינוי יחס גובה-רוחב"</string> - <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"בעיות במצלמה?\nאפשר להקיש כדי לבצע התאמה מחדש"</string> - <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"הבעיה לא נפתרה?\nאפשר להקיש כדי לחזור לגרסה הקודמת"</string> - <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"אין בעיות במצלמה? אפשר להקיש כדי לסגור."</string> + <string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"בעיות במצלמה?\nאפשר ללחוץ כדי לבצע התאמה מחדש"</string> + <string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"הבעיה לא נפתרה?\nאפשר ללחוץ כדי לחזור לגרסה הקודמת"</string> + <string name="camera_compat_dismiss_button_description" msgid="2795364433503817511">"אין בעיות במצלמה? אפשר ללחוץ כדי לסגור."</string> <string name="windowing_app_handle_education_tooltip" msgid="2929643449849791854">"תפריט האפליקציה נמצא כאן"</string> <string name="windowing_desktop_mode_image_button_education_tooltip" msgid="2523468503353474095">"כדי לפתוח כמה אפליקציות יחד, צריך לעבור למצב תצוגה למחשב"</string> <string name="windowing_desktop_mode_exit_education_tooltip" msgid="5225660258192054132">"אפשר לחזור למסך מלא בכל שלב מתפריט האפליקציה"</string> <string name="letterbox_education_dialog_title" msgid="7739895354143295358">"רוצה לראות ולעשות יותר?"</string> <string name="letterbox_education_split_screen_text" msgid="449233070804658627">"צריך לגרור אפליקציה אחרת כדי להשתמש במסך המפוצל"</string> - <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"צריך להקיש הקשה כפולה מחוץ לאפליקציה כדי למקם אותה מחדש"</string> + <string name="letterbox_education_reposition_text" msgid="4589957299813220661">"צריך ללחוץ לחיצה כפולה מחוץ לאפליקציה כדי למקם אותה מחדש"</string> <string name="letterbox_education_got_it" msgid="4057634570866051177">"הבנתי"</string> <string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"מרחיבים כדי לקבל מידע נוסף."</string> <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"להפעיל מחדש לתצוגה טובה יותר?"</string> @@ -110,7 +110,7 @@ <string name="letterbox_restart_cancel" msgid="1342209132692537805">"ביטול"</string> <string name="letterbox_restart_restart" msgid="8529976234412442973">"הפעלה מחדש"</string> <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"לא להציג שוב"</string> - <string name="letterbox_reachability_reposition_text" msgid="3522042240665748268">"אפשר להקיש הקשה כפולה כדי\nלהעביר את האפליקציה למקום אחר"</string> + <string name="letterbox_reachability_reposition_text" msgid="3522042240665748268">"אפשר ללחוץ לחיצה כפולה כדי\nלהעביר את האפליקציה למקום אחר"</string> <string name="maximize_button_text" msgid="1650859196290301963">"הגדלה"</string> <string name="minimize_button_text" msgid="271592547935841753">"מזעור"</string> <string name="close_button_text" msgid="2913281996024033299">"סגירה"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 9e0f107f7e55..abc4d08cd3ca 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -25,7 +25,7 @@ <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> చిత్రంలో చిత్రం రూపంలో ఉంది"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ఈ లక్షణాన్ని ఉపయోగించకూడదు అని మీరు అనుకుంటే, సెట్టింగ్లను తెరవడానికి ట్యాప్ చేసి, దీన్ని ఆఫ్ చేయండి."</string> <string name="pip_play" msgid="3496151081459417097">"ప్లే చేయి"</string> - <string name="pip_pause" msgid="690688849510295232">"పాజ్ చేయి"</string> + <string name="pip_pause" msgid="690688849510295232">"పాజ్ చేయండి"</string> <string name="pip_skip_to_next" msgid="8403429188794867653">"దాటవేసి తర్వాత దానికి వెళ్లు"</string> <string name="pip_skip_to_prev" msgid="7172158111196394092">"దాటవేసి మునుపటి దానికి వెళ్లు"</string> <string name="accessibility_action_pip_resize" msgid="4623966104749543182">"సైజ్ మార్చు"</string> diff --git a/libs/WindowManager/Shell/shared/Android.bp b/libs/WindowManager/Shell/shared/Android.bp index c3ee0f76f550..d7669ed5cf34 100644 --- a/libs/WindowManager/Shell/shared/Android.bp +++ b/libs/WindowManager/Shell/shared/Android.bp @@ -56,6 +56,7 @@ android_library { static_libs: [ "androidx.core_core-animation", "androidx.dynamicanimation_dynamicanimation", + "com_android_wm_shell_flags_lib", "jsr330", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index e033f673d07d..840de2c86f92 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -40,7 +40,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; -import android.app.TaskInfo; import android.app.WindowConfiguration; import android.graphics.Rect; import android.util.ArrayMap; @@ -57,6 +56,9 @@ public class TransitionUtil { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; + /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */ + public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 1; + /** @return true if the transition was triggered by opening something vs closing something */ public static boolean isOpeningType(@WindowManager.TransitionType int type) { return type == TRANSIT_OPEN diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleAnythingFlagHelper.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleAnythingFlagHelper.java new file mode 100644 index 000000000000..e1f1d0c32eb0 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleAnythingFlagHelper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles; + +import com.android.wm.shell.Flags; + +/** + * Bubble anything has some dependent flags, this class simplifies the checks. + * (TODO: b/389737359 - remove this when the feature is launched). + */ +public class BubbleAnythingFlagHelper { + + private BubbleAnythingFlagHelper() {} + + /** Whether creating any bubble or the overall bubble anything feature is enabled. */ + public static boolean enableCreateAnyBubble() { + return enableBubbleAnything() || Flags.enableCreateAnyBubble(); + } + + /** + * Whether creating any bubble and transforming to fullscreen, or the overall bubble anything + * feature is enabled. + */ + public static boolean enableBubbleToFullscreen() { + return enableBubbleAnything() + || (Flags.enableBubbleToFullscreen() + && Flags.enableCreateAnyBubble()); + } + + /** Whether the overall bubble anything feature is enabled. */ + public static boolean enableBubbleAnything() { + return Flags.enableBubbleAnything(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java index 8cd7b0f48003..10023c9dba40 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/appzoomout/AppZoomOutController.java @@ -16,7 +16,9 @@ package com.android.wm.shell.appzoomout; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.Flags.spatialModelAppPushback; import android.app.ActivityManager; import android.app.WindowConfiguration; @@ -92,7 +94,9 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController mDisplayAreaOrganizer = displayAreaOrganizer; mMainExecutor = mainExecutor; - shellInit.addInitCallback(this::onInit, this); + if (spatialModelAppPushback()) { + shellInit.addInitCallback(this::onInit, this); + } } private void onInit() { @@ -100,6 +104,7 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); mDisplayController.addDisplayChangingController(this); + updateDisplayLayout(mContext.getDisplayId()); mDisplayAreaOrganizer.registerOrganizer(); } @@ -135,7 +140,9 @@ public class AppZoomOutController implements RemoteCallable<AppZoomOutController public void onDisplayChange(int displayId, int fromRotation, int toRotation, @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) { // TODO: verify if there is synchronization issues. - mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + if (toRotation != ROTATION_UNDEFINED) { + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt index f8f284238a98..8171312762ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/automotive/AutoTaskStackControllerImpl.kt @@ -33,7 +33,7 @@ import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction -import com.android.systemui.car.Flags.autoTaskStackWindowing +import com.android.wm.shell.Flags.enableAutoTaskStackController import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.common.ShellExecutor @@ -66,7 +66,7 @@ class AutoTaskStackControllerImpl @Inject constructor( private val defaultRootTaskPerDisplay = mutableMapOf<Int, Int>() init { - if (!autoTaskStackWindowing()) { + if (!enableAutoTaskStackController()) { throw IllegalStateException("Failed to initialize" + "AutoTaskStackController as the auto_task_stack_windowing TS flag is disabled.") } else { @@ -220,7 +220,7 @@ class AutoTaskStackControllerImpl @Inject constructor( displayId: Int, listener: RootTaskStackListener ) { - if (!autoTaskStackWindowing()) { + if (!enableAutoTaskStackController()) { Slog.e( TAG, "Failed to create root task stack as the " + "auto_task_stack_windowing TS flag is disabled." @@ -236,7 +236,7 @@ class AutoTaskStackControllerImpl @Inject constructor( } override fun setDefaultRootTaskStackOnDisplay(displayId: Int, rootTaskStackId: Int?) { - if (!autoTaskStackWindowing()) { + if (!enableAutoTaskStackController()) { Slog.e( TAG, "Failed to set default root task stack as the " + "auto_task_stack_windowing TS flag is disabled." @@ -280,7 +280,7 @@ class AutoTaskStackControllerImpl @Inject constructor( } override fun startTransition(transaction: AutoTaskStackTransaction): IBinder? { - if (!autoTaskStackWindowing()) { + if (!enableAutoTaskStackController()) { Slog.e( TAG, "Failed to start transaction as the " + "auto_task_stack_windowing TS flag is disabled." diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 862906a11424..62995319db80 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -136,23 +136,15 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl // Update bitmap val fg = InsetDrawable(overflowBtn?.iconDrawable, overflowIconInset) - bitmap = - iconFactory - .createBadgedIconBitmap(AdaptiveIconDrawable(ColorDrawable(colorAccent), fg)) - .icon + val drawable = AdaptiveIconDrawable(ColorDrawable(colorAccent), fg) + bitmap = iconFactory.createBadgedIconBitmap(drawable).icon // Update dot path dotPath = PathParser.createPathFromPathData( res.getString(com.android.internal.R.string.config_icon_mask) ) - val scale = - iconFactory.normalizer.getScale( - iconView!!.iconDrawable, - null /* outBounds */, - null /* path */, - null /* outMaskShape */ - ) + val scale = iconFactory.normalizer.getScale(iconView!!.iconDrawable) val radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f val matrix = Matrix() matrix.setScale( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index e073b02dc630..ac5b9c9866ed 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -674,9 +674,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView if (mTaskView != null) { mTaskView.getBoundsOnScreen(mTempBounds); } - // return the bottom of the content rect, adjusted for insets so the result is in screen - // coordinate - return mTempBounds.bottom + mPositioner.getInsets().top; + return mTempBounds.bottom; } /** Update the amount by which to clip the expanded view at the bottom. */ 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 eb1e72790a25..c9890a5b4963 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 @@ -447,8 +447,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - private int imeTop(float surfaceOffset) { - return mImeFrame.top + (int) surfaceOffset; + private int imeTop(float surfaceOffset, float surfacePositionY) { + // surfaceOffset is already offset by the surface's top inset, so we need to subtract + // the top inset so that the return value is in screen coordinates. + return mImeFrame.top + (int) (surfaceOffset - surfacePositionY); } private boolean calcIsFloating(InsetsSource imeSource) { @@ -581,7 +583,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged final float alpha = (mAnimateAlpha || isFloating) ? (value - hiddenY) / (shownY - hiddenY) : 1f; t.setAlpha(animatingLeash, alpha); - dispatchPositionChanged(mDisplayId, imeTop(value), t); + dispatchPositionChanged(mDisplayId, imeTop(value, defaultY), t); t.apply(); mTransactionPool.release(t); }); @@ -600,11 +602,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged t.setPosition(animatingLeash, x, value); if (DEBUG) { Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:" - + imeTop(hiddenY) + "->" + imeTop(shownY) + + imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY) + " showing:" + (mAnimationDirection == DIRECTION_SHOW)); } - int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY), - imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t); + int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY), + imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW, + isFloating, t); mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0; final float alpha = (mAnimateAlpha || isFloating) ? (value - hiddenY) / (shownY - hiddenY) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index cdfa14bbc4e2..9b9988457808 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.desktopmode +import com.android.window.flags.Flags import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter @@ -26,35 +27,28 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl override fun onShellCommand(args: Array<String>, pw: PrintWriter): Boolean = when (args[0]) { - "moveToDesktop" -> { - if (!runMoveToDesktop(args, pw)) { - pw.println("Task not found. Please enter a valid taskId.") - false - } else { - true - } - } - "moveToNextDisplay" -> { - if (!runMoveToNextDisplay(args, pw)) { - pw.println("Task not found. Please enter a valid taskId.") - false - } else { - true - } - } + "moveTaskToDesk" -> runMoveTaskToDesk(args, pw) + "moveToNextDisplay" -> runMoveToNextDisplay(args, pw) + "createDesk" -> runCreateDesk(args, pw) + "activateDesk" -> runActivateDesk(args, pw) + "removeDesk" -> runRemoveDesk(args, pw) + "removeAllDesks" -> runRemoveAllDesks(args, pw) + "moveTaskToFront" -> runMoveTaskToFront(args, pw) + "moveTaskOutOfDesk" -> runMoveTaskOutOfDesk(args, pw) + "canCreateDesk" -> runCanCreateDesk(args, pw) + "getActiveDeskId" -> runGetActiveDeskId(args, pw) else -> { pw.println("Invalid command: ${args[0]}") false } } - private fun runMoveToDesktop(args: Array<String>, pw: PrintWriter): Boolean { + private fun runMoveTaskToDesk(args: Array<String>, pw: PrintWriter): Boolean { if (args.size < 2) { // First argument is the action name. pw.println("Error: task id should be provided as arguments") return false } - val taskId = try { args[1].toInt() @@ -62,7 +56,22 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - return controller.moveTaskToDesktop(taskId, transitionSource = UNKNOWN) + if (!Flags.enableMultipleDesktopsBackend()) { + return controller.moveTaskToDesktop(taskId, transitionSource = UNKNOWN) + } + if (args.size < 3) { + pw.println("Error: desk id should be provided as arguments") + return false + } + val deskId = + try { + args[2].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: desk id should be an integer") + return false + } + pw.println("Not implemented.") + return false } private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { @@ -84,10 +93,184 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl return true } + private fun runCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: desk id should be provided as arguments") + return false + } + val displayId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: display id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: desk id should be provided as arguments") + return false + } + val deskId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: desk id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: desk id should be provided as arguments") + return false + } + val deskId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: desk id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runMoveTaskToFront(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + val taskId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runMoveTaskOutOfDesk(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + val taskId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runCanCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + val displayId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: display id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + + private fun runGetActiveDeskId(args: Array<String>, pw: PrintWriter): Boolean { + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("Not supported.") + return false + } + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + val displayId = + try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: display id should be an integer") + return false + } + pw.println("Not implemented.") + return false + } + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { - pw.println("$prefix moveToDesktop <taskId> ") - pw.println("$prefix Move a task with given id to desktop mode.") - pw.println("$prefix moveToNextDisplay <taskId> ") + if (!Flags.enableMultipleDesktopsBackend()) { + pw.println("$prefix moveTaskToDesk <taskId> ") + pw.println("$prefix Move a task with given id to desktop mode.") + pw.println("$prefix moveToNextDisplay <taskId> ") + pw.println("$prefix Move a task with given id to next display.") + return + } + pw.println("$prefix moveTaskToDesk <taskId> <deskId>") + pw.println("$prefix Move a task with given id to the given desk and activate it.") + pw.println("$prefix moveToNextDisplay <taskId>") pw.println("$prefix Move a task with given id to next display.") + pw.println("$prefix createDesk <displayId>") + pw.println("$prefix Creates a desk on the given display.") + pw.println("$prefix activateDesk <deskId>") + pw.println("$prefix Activates the given desk.") + pw.println("$prefix removeDesk <deskId> ") + pw.println("$prefix Removes the given desk and all of its windows.") + pw.println("$prefix removeAllDesks") + pw.println("$prefix Removes all the desks and their windows across all displays") + pw.println("$prefix moveTaskToFront <taskId>") + pw.println("$prefix Moves a task in front of its siblings.") + pw.println("$prefix moveTaskOutOfDesk <taskId>") + pw.println("$prefix Moves the given desktop task out of the desk into fullscreen mode.") + pw.println("$prefix canCreateDesk <displayId>") + pw.println("$prefix Whether creating a new desk in the given display is allowed.") + pw.println("$prefix getActivateDeskId <displayId>") + pw.println("$prefix Print the id of the active desk in the given display.") } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 73d15270c811..f38957e48dbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -552,7 +552,11 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - moveHomeTask(wct, toTop = true) + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { + moveHomeTask(wct, toTop = true, taskInfo.displayId) + } else { + moveHomeTask(wct, toTop = true) + } val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) @@ -1309,11 +1313,15 @@ class DesktopTasksController( ): Int? { logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront) // Move home to front, ensures that we go back home when all desktop windows are closed - moveHomeTask(wct, toTop = true) + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { + moveHomeTask(wct, toTop = true, displayId) + } else { + moveHomeTask(wct, toTop = true) + } // Currently, we only handle the desktop on the default display really. if ( - (displayId == DEFAULT_DISPLAY || Flags.enableBugFixesForSecondaryDisplay()) && + (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) && ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() ) { // Add translucent wallpaper activity to show the wallpaper underneath @@ -1359,9 +1367,13 @@ class DesktopTasksController( return taskIdToMinimize } - private fun moveHomeTask(wct: WindowContainerTransaction, toTop: Boolean) { + private fun moveHomeTask( + wct: WindowContainerTransaction, + toTop: Boolean, + displayId: Int = DEFAULT_DISPLAY, + ) { shellTaskOrganizer - .getRunningTasks(context.displayId) + .getRunningTasks(displayId) .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ toTop) } } @@ -1370,12 +1382,19 @@ class DesktopTasksController( logV("addWallpaperActivity") if (Flags.enableDesktopWallpaperActivityForSystemUser()) { val intent = Intent(context, DesktopWallpaperActivity::class.java) + if ( + desktopWallpaperActivityTokenProvider.getToken(displayId) == null && + Flags.enablePerDisplayDesktopWallpaperActivity() + ) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + } val options = ActivityOptions.makeBasic().apply { launchWindowingMode = WINDOWING_MODE_FULLSCREEN pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - if (Flags.enableBugFixesForSecondaryDisplay()) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { launchDisplayId = displayId } } @@ -1391,13 +1410,20 @@ class DesktopTasksController( val userHandle = UserHandle.of(userId) val userContext = context.createContextAsUser(userHandle, /* flags= */ 0) val intent = Intent(userContext, DesktopWallpaperActivity::class.java) + if ( + desktopWallpaperActivityTokenProvider.getToken(displayId) == null && + Flags.enablePerDisplayDesktopWallpaperActivity() + ) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + } intent.putExtra(Intent.EXTRA_USER_HANDLE, userId) val options = ActivityOptions.makeBasic().apply { launchWindowingMode = WINDOWING_MODE_FULLSCREEN pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS - if (Flags.enableBugFixesForSecondaryDisplay()) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { launchDisplayId = displayId } } @@ -1414,8 +1440,8 @@ class DesktopTasksController( } } - private fun removeWallpaperActivity(wct: WindowContainerTransaction) { - desktopWallpaperActivityTokenProvider.getToken()?.let { token -> + private fun removeWallpaperActivity(wct: WindowContainerTransaction, displayId: Int) { + desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token -> logV("removeWallpaperActivity") if (Flags.enableDesktopWallpaperActivityForSystemUser()) { wct.reorder(token, /* onTop= */ false) @@ -1440,9 +1466,6 @@ class DesktopTasksController( if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { return } - if (displayId != DEFAULT_DISPLAY) { - return - } } else if ( Flags.enableDesktopWindowingPip() && taskRepository.isMinimizedPipPresentInDisplay(displayId) && @@ -1457,7 +1480,7 @@ class DesktopTasksController( desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( FULLSCREEN_ANIMATION_DURATION ) - removeWallpaperActivity(wct) + removeWallpaperActivity(wct, displayId) } fun releaseVisualIndicator() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index b9a65fee7d4d..14c8429766cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -60,7 +60,9 @@ class DesktopTasksTransitionObserver( shellInit: ShellInit, ) : Transitions.TransitionObserver { - private var transitionToCloseWallpaper: IBinder? = null + data class CloseWallpaperTransition(val transition: IBinder, val displayId: Int) + + private var transitionToCloseWallpaper: CloseWallpaperTransition? = null /* Pending PiP transition and its associated display id and task id. */ private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null private var currentProfileId: Int @@ -248,9 +250,10 @@ class DesktopTasksTransitionObserver( desktopRepository.getVisibleTaskCount(taskInfo.displayId) == 0 && change.mode == TRANSIT_CLOSE && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM && - desktopWallpaperActivityTokenProvider.getToken() != null + desktopWallpaperActivityTokenProvider.getToken(taskInfo.displayId) != null ) { - transitionToCloseWallpaper = transition + transitionToCloseWallpaper = + CloseWallpaperTransition(transition, taskInfo.displayId) currentProfileId = taskInfo.userId } } @@ -265,25 +268,28 @@ class DesktopTasksTransitionObserver( } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + val lastSeenTransitionToCloseWallpaper = transitionToCloseWallpaper // TODO: b/332682201 Update repository state - if (transitionToCloseWallpaper == transition) { + if (lastSeenTransitionToCloseWallpaper?.transition == transition) { // TODO: b/362469671 - Handle merging the animation when desktop is also closing. - desktopWallpaperActivityTokenProvider.getToken()?.let { wallpaperActivityToken -> - if (Flags.enableDesktopWallpaperActivityForSystemUser()) { - transitions.startTransition( - TRANSIT_TO_BACK, - WindowContainerTransaction() - .reorder(wallpaperActivityToken, /* onTop= */ false), - null, - ) - } else { - transitions.startTransition( - TRANSIT_CLOSE, - WindowContainerTransaction().removeTask(wallpaperActivityToken), - null, - ) + desktopWallpaperActivityTokenProvider + .getToken(lastSeenTransitionToCloseWallpaper.displayId) + ?.let { wallpaperActivityToken -> + if (Flags.enableDesktopWallpaperActivityForSystemUser()) { + transitions.startTransition( + TRANSIT_TO_BACK, + WindowContainerTransaction() + .reorder(wallpaperActivityToken, /* onTop= */ false), + null, + ) + } else { + transitions.startTransition( + TRANSIT_CLOSE, + WindowContainerTransaction().removeTask(wallpaperActivityToken), + null, + ) + } } - } transitionToCloseWallpaper = null } else if (pendingPipTransitionAndPipTask?.first == transition) { val desktopRepository = desktopUserRepositories.getProfile(currentProfileId) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 6e7740dc13e3..91150b0070ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -129,6 +129,20 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** + * Called when the Shell wants to start an exit-via-expand from Pip transition/animation. + */ + public void startExpandTransition(WindowContainerTransaction out) { + // Default implementation does nothing. + } + + /** + * Called when the Shell wants to start a remove Pip transition/animation. + */ + public void startRemoveTransition(boolean withFadeout) { + // Default implementation does nothing. + } + + /** * Called when the Shell wants to start resizing Pip transition/animation. * * @param duration the suggested duration for resize animation. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index 9babe9e9e4eb..e22cd37ab4b7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -344,13 +344,22 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, */ @Override public void dismissPip() { + dismissPip(true /* withFadeout */); + } + + /** + * Dismisses the pinned stack. + * + * @param withFadeout should animate with fadeout for the removal + */ + public void dismissPip(boolean withFadeout) { if (DEBUG) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePip: callers=\n%s", TAG, Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */); - mPipScheduler.scheduleRemovePip(); + mPipScheduler.scheduleRemovePip(withFadeout); } /** Sets the movement bounds to use to constrain PIP position animations. */ @@ -473,7 +482,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2, 0, mSpringConfig) - .withEndActions(this::dismissPip); + .withEndActions(() -> dismissPip(false /* withFadeout */)); startBoundsAnimator( getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */); @@ -772,7 +781,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, case PipTransitionState.EXITING_PIP: // We need to force finish any local animators if about to leave PiP, to avoid // breaking the state (e.g. leashes are cleaned up upon exit). - if (!mPipBoundsState.getMotionBoundsState().isInMotion()) break; cancelPhysicsAnimation(); settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */); break; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index ea8dac982703..ed532cad0523 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -19,9 +19,6 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; -import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; - import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; @@ -112,19 +109,6 @@ public class PipScheduler { return wct; } - @Nullable - private WindowContainerTransaction getRemovePipTransaction() { - WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); - if (pipTaskToken == null) { - return null; - } - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(pipTaskToken, null); - wct.setWindowingMode(pipTaskToken, WINDOWING_MODE_UNDEFINED); - wct.reorder(pipTaskToken, false); - return wct; - } - /** * Schedules exit PiP via expand transition. */ @@ -133,21 +117,16 @@ public class PipScheduler { if (!mPipTransitionState.isInPip()) return; WindowContainerTransaction wct = getExitPipViaExpandTransaction(); if (wct != null) { - mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, - null /* destinationBounds */); + mPipTransitionController.startExpandTransition(wct); } }); } /** Schedules remove PiP transition. */ - public void scheduleRemovePip() { + public void scheduleRemovePip(boolean withFadeout) { mMainExecutor.execute(() -> { if (!mPipTransitionState.isInPip()) return; - WindowContainerTransaction wct = getRemovePipTransaction(); - if (wct != null) { - mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct, - null /* destinationBounds */); - } + mPipTransitionController.startRemoveTransition(withFadeout); }); } @@ -216,9 +195,11 @@ public class PipScheduler { * @param degrees the angle to rotate the bounds to. */ public void scheduleUserResizePip(Rect toBounds, float degrees) { - if (toBounds.isEmpty()) { + if (toBounds.isEmpty() || !mPipTransitionState.isInPip()) { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Attempted to user resize PIP to empty bounds, aborting.", TAG); + "%s: Attempted to user resize PIP in invalid state, aborting;" + + "toBounds=%s, mPipTransitionState=%s", + TAG, toBounds, mPipTransitionState); return; } SurfaceControl leash = mPipTransitionState.getPinnedTaskLeash(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 0a42c71ce095..4902455cae16 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -18,6 +18,7 @@ package com.android.wm.shell.pip2.phone; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -126,6 +127,7 @@ public class PipTransition extends PipTransitionController implements @Nullable private IBinder mResizeTransition; private int mBoundsChangeDuration = BOUNDS_CHANGE_JUMPCUT_DURATION; + private boolean mPendingRemoveWithFadeout; // @@ -184,15 +186,19 @@ public class PipTransition extends PipTransitionController implements // @Override - public void startExitTransition(int type, WindowContainerTransaction out, - @Nullable Rect destinationBounds) { - if (out == null) { - return; - } - IBinder transition = mTransitions.startTransition(type, out, this); - if (type == TRANSIT_EXIT_PIP) { - mExitViaExpandTransition = transition; - } + public void startExpandTransition(WindowContainerTransaction out) { + if (out == null) return; + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); + mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); + } + + @Override + public void startRemoveTransition(boolean withFadeout) { + final WindowContainerTransaction wct = getRemovePipTransaction(); + if (wct == null) return; + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); + mPendingRemoveWithFadeout = withFadeout; + mTransitions.startTransition(TRANSIT_REMOVE_PIP, wct, this); } @Override @@ -284,7 +290,6 @@ public class PipTransition extends PipTransitionController implements finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; - mPipTransitionState.setState(PipTransitionState.EXITING_PIP); return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; @@ -690,11 +695,19 @@ public class PipTransition extends PipTransitionController implements TransitionInfo.Change pipChange = getChangeByToken(info, mPipTransitionState.getPipTaskToken()); mFinishCallback = finishCallback; - PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipChange.getLeash(), - startTransaction, PipAlphaAnimator.FADE_OUT); + finishTransaction.setAlpha(pipChange.getLeash(), 0f); - animator.setAnimationEndCallback(this::finishTransition); - animator.start(); + if (mPendingRemoveWithFadeout) { + PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipChange.getLeash(), + startTransaction, PipAlphaAnimator.FADE_OUT); + animator.setAnimationEndCallback(this::finishTransition); + animator.start(); + } else { + // Jumpcut to a faded-out PiP if no fadeout animation was requested. + startTransaction.setAlpha(pipChange.getLeash(), 0f); + startTransaction.apply(); + finishTransition(); + } return true; } @@ -824,6 +837,19 @@ public class PipTransition extends PipTransitionController implements return wct; } + @Nullable + private WindowContainerTransaction getRemovePipTransaction() { + WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken(); + if (pipTaskToken == null) { + return null; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(pipTaskToken, null); + wct.setWindowingMode(pipTaskToken, WINDOWING_MODE_UNDEFINED); + wct.reorder(pipTaskToken, false); + return wct; + } + private boolean isAutoEnterInButtonNavigation(@NonNull TransitionRequestInfo requestInfo) { final ActivityManager.RunningTaskInfo pipTask = requestInfo.getPipChange() != null ? requestInfo.getPipChange().getTaskInfo() : null; @@ -1000,6 +1026,7 @@ public class PipTransition extends PipTransitionController implements } mPipTransitionState.setPinnedTaskLeash(null); mPipTransitionState.setPipTaskInfo(null); + mPendingRemoveWithFadeout = false; break; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 6f9f40a487c7..8805cbb0dfbd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -27,7 +27,9 @@ import android.window.WindowContainerToken; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.protolog.ProtoLog; import com.android.internal.util.Preconditions; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.annotations.ShellMainThread; import java.io.PrintWriter; @@ -201,6 +203,13 @@ public class PipTransitionState { Preconditions.checkArgument(extra != null && !extra.isEmpty(), "No extra bundle for " + stateToString(state) + " state."); } + if (!shouldTransitionToState(state)) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Attempted to transition to an invalid state=%s, while in %s", + TAG, stateToString(state), this); + return; + } + if (mState != state) { final int prevState = mState; mState = state; @@ -374,6 +383,17 @@ public class PipTransitionState { return ++mPrevCustomState; } + private boolean shouldTransitionToState(@TransitionState int newState) { + switch (newState) { + case SCHEDULED_BOUNDS_CHANGE: + // Allow scheduling bounds change only while in PiP, except for if another bounds + // change was scheduled but hasn't started playing yet. + return isInPip(); + default: + return true; + } + } + private static String stateToString(int state) { switch (state) { case UNDEFINED: return "undefined"; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index d6f9183a4281..0182588398a4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -54,7 +54,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.launcher3.Flags; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; @@ -103,6 +102,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private final RecentTasksImpl mImpl = new RecentTasksImpl(); private final ActivityTaskManager mActivityTaskManager; private final TaskStackTransitionObserver mTaskStackTransitionObserver; + private final RecentsShellCommandHandler mRecentsShellCommandHandler; private RecentsTransitionHandler mTransitionHandler = null; private IRecentTasksListener mListener; private final boolean mPcFeatureEnabled; @@ -167,6 +167,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mDesktopUserRepositories = desktopUserRepositories; mTaskStackTransitionObserver = taskStackTransitionObserver; mMainExecutor = mainExecutor; + mRecentsShellCommandHandler = new RecentsShellCommandHandler(this); shellInit.addInitCallback(this::onInit, this); } @@ -183,6 +184,7 @@ public class RecentTasksController implements TaskStackListenerCallback, mShellController.addExternalInterface(IRecentTasks.DESCRIPTOR, this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); + mShellCommandHandler.addCommandCallback("recents", mRecentsShellCommandHandler, this); mUserId = ActivityManager.getCurrentUser(); mDesktopUserRepositories.ifPresent( desktopUserRepositories -> @@ -550,9 +552,7 @@ public class RecentTasksController implements TaskStackListenerCallback, groupedTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo, mTaskSplitBoundsMap.get(pairedTaskId))); } else { - if ( - Flags.enableUseTopVisibleActivityForExcludeFromRecentTask() - && isWallpaperTask(taskInfo)) { + if (isWallpaperTask(taskInfo)) { // Don't add the wallpaper task as an entry in grouped tasks continue; } @@ -656,6 +656,11 @@ public class RecentTasksController implements TaskStackListenerCallback, return mActivityTaskManager.removeTask(taskId); } + /** Removes all recent tasks that are visible. */ + public void removeAllVisibleRecentTasks() throws RemoteException { + ActivityTaskManager.getService().removeAllVisibleRecentTasks(); + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; pw.println(prefix + TAG); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsShellCommandHandler.kt new file mode 100644 index 000000000000..f786e079ed93 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsShellCommandHandler.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.recents + +import android.os.RemoteException +import com.android.wm.shell.sysui.ShellCommandHandler.ShellCommandActionHandler +import java.io.PrintWriter + +class RecentsShellCommandHandler( + private val recentTasksController: RecentTasksController +) : ShellCommandActionHandler { + override fun onShellCommand(args: Array<out String>, pw: PrintWriter): Boolean { + when (args[0]) { + "clearAll" -> return runClearAll(pw) + else -> { + pw.println("Invalid command: " + args[0]) + return false + } + } + } + + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { + pw.println("${prefix}clearAll") + pw.println("$prefix Clears all visible recent tasks.") + } + + private fun runClearAll(pw: PrintWriter): Boolean { + try { + recentTasksController.removeAllVisibleRecentTasks() + } catch (e: RemoteException) { + pw.println("Exception while removing visible recent tasks:") + e.printStackTrace(pw) + return false + } + return true + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index c27ef295db51..c9136b4ad18d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -929,8 +929,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, for (int taskId : taskIds) { ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); if (task != null) { - wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) - .setBounds(task.token, null); + wct.setWindowingMode(task.getToken(), WINDOWING_MODE_UNDEFINED) + .setBounds(task.getToken(), null); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java index 81f444ba2af3..c5994f83429a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java @@ -633,7 +633,7 @@ public class SplashscreenContentDrawer { private class ShapeIconFactory extends BaseIconFactory { protected ShapeIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) { - super(context, fillResIconDpi, iconBitmapSize, true /* shapeDetection */); + super(context, fillResIconDpi, iconBitmapSize); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 8dd14983e122..5c7dd078ee45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -536,9 +536,12 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener { } return; } + // Cache it to avoid NPE and make sure to remove it from recents history. + // mTaskToken can be cleared in onTaskVanished() when the task is removed. + final WindowContainerToken taskToken = mTaskToken; mShellExecutor.execute(() -> { WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.removeTask(mTaskToken); + wct.removeTask(taskToken); mTaskViewTransitions.closeTaskView(wct, this); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java index 6d01e247b48d..e04682a4b86f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java @@ -17,6 +17,8 @@ package com.android.wm.shell.transition; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; @@ -66,36 +68,45 @@ public class FocusTransitionObserver { if (!enableDisplayFocusInShellTransitions()) { return; } + final SparseArray<RunningTaskInfo> lastTransitionFocusedTasks = + mFocusedTaskOnDisplay.clone(); + final List<TransitionInfo.Change> changes = info.getChanges(); for (int i = changes.size() - 1; i >= 0; i--) { final TransitionInfo.Change change = changes.get(i); final RunningTaskInfo task = change.getTaskInfo(); - if (task != null - && (change.hasFlags(FLAG_MOVED_TO_TOP) || change.getMode() == TRANSIT_OPEN)) { - final RunningTaskInfo lastFocusedTaskOnDisplay = - mFocusedTaskOnDisplay.get(task.displayId); - if (lastFocusedTaskOnDisplay != null) { - mTmpTasksToBeNotified.add(lastFocusedTaskOnDisplay); + if (task != null) { + if (change.hasFlags(FLAG_MOVED_TO_TOP) || change.getMode() == TRANSIT_OPEN) { + updateFocusedTaskPerDisplay(task, task.displayId); + } else { + // Update focus assuming that any task moved to another display is focused in + // the new display. + // TODO(sahok): remove this logic when b/388665104 is fixed + final boolean isBeyondDisplay = change.getStartDisplayId() != INVALID_DISPLAY + && change.getEndDisplayId() != INVALID_DISPLAY + && change.getStartDisplayId() != change.getEndDisplayId(); + + RunningTaskInfo lastTransitionFocusedTaskOnStartDisplay = + lastTransitionFocusedTasks.get(change.getStartDisplayId()); + final boolean isLastTransitionFocused = + lastTransitionFocusedTaskOnStartDisplay != null + && task.taskId + == lastTransitionFocusedTaskOnStartDisplay.taskId; + if (change.getMode() == TRANSIT_CHANGE && isBeyondDisplay + && isLastTransitionFocused) { + // The task have moved to another display and keeps its focus. + // MOVE_TO_TOP is not reported but we need to update the focused task in + // the end display. + updateFocusedTaskPerDisplay(task, change.getEndDisplayId()); + } } - mTmpTasksToBeNotified.add(task); - mFocusedTaskOnDisplay.put(task.displayId, task); } + if (change.hasFlags(FLAG_IS_DISPLAY) && change.hasFlags(FLAG_MOVED_TO_TOP)) { if (mFocusedDisplayId != change.getEndDisplayId()) { - final RunningTaskInfo lastGloballyFocusedTask = - mFocusedTaskOnDisplay.get(mFocusedDisplayId); - if (lastGloballyFocusedTask != null) { - mTmpTasksToBeNotified.add(lastGloballyFocusedTask); - } - mFocusedDisplayId = change.getEndDisplayId(); - notifyFocusedDisplayChanged(); - final RunningTaskInfo currentGloballyFocusedTask = - mFocusedTaskOnDisplay.get(mFocusedDisplayId); - if (currentGloballyFocusedTask != null) { - mTmpTasksToBeNotified.add(currentGloballyFocusedTask); - } + updateFocusedDisplay(change.getEndDisplayId()); } } } @@ -103,6 +114,31 @@ public class FocusTransitionObserver { mTmpTasksToBeNotified.clear(); } + private void updateFocusedTaskPerDisplay(RunningTaskInfo task, int displayId) { + final RunningTaskInfo lastFocusedTaskOnDisplay = + mFocusedTaskOnDisplay.get(displayId); + if (lastFocusedTaskOnDisplay != null) { + mTmpTasksToBeNotified.add(lastFocusedTaskOnDisplay); + } + mTmpTasksToBeNotified.add(task); + mFocusedTaskOnDisplay.put(displayId, task); + } + + private void updateFocusedDisplay(int endDisplayId) { + final RunningTaskInfo lastGloballyFocusedTask = + mFocusedTaskOnDisplay.get(mFocusedDisplayId); + if (lastGloballyFocusedTask != null) { + mTmpTasksToBeNotified.add(lastGloballyFocusedTask); + } + mFocusedDisplayId = endDisplayId; + notifyFocusedDisplayChanged(); + final RunningTaskInfo currentGloballyFocusedTask = + mFocusedTaskOnDisplay.get(mFocusedDisplayId); + if (currentGloballyFocusedTask != null) { + mTmpTasksToBeNotified.add(currentGloballyFocusedTask); + } + } + /** * Sets the focus transition listener that receives any transitions resulting in focus switch. * This is for calls from outside the Shell, within the host process. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index d5929f010e02..abfb41bb513a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -39,6 +39,7 @@ import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPI import static com.android.systemui.shared.Flags.returnAnimationFrameworkLongLived; import static com.android.window.flags.Flags.ensureWallpaperInTransitions; +import static com.android.wm.shell.shared.TransitionUtil.FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; @@ -84,6 +85,7 @@ import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes; +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.FocusTransitionListener; @@ -795,6 +797,14 @@ public class Transitions implements RemoteCallable<Transitions>, mReadyDuringSync.remove(active); } + // If any of the changes are on DesktopWallpaperActivity, add the flag to the change. + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && DesktopWallpaperActivity.isWallpaperTask(change.getTaskInfo())) { + change.setFlags(FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY); + } + } + final Track track = getOrCreateTrack(info.getTrack()); track.mReadyTransitions.add(active); 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 a90c6fbdae1d..d6d393f2500c 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 @@ -2022,7 +2022,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Supplier<SurfaceControl.Transaction> transactionFactory, Handler handler) { final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled() - ? new VeiledResizeTaskPositioner( + // TODO(b/383632995): Update when the flag is launched. + ? (Flags.enableConnectedDisplaysWindowDrag() + ? new MultiDisplayVeiledResizeTaskPositioner( taskOrganizer, windowDecoration, displayController, @@ -2030,6 +2032,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, transitions, interactionJankMonitor, handler) + : new VeiledResizeTaskPositioner( + taskOrganizer, + windowDecoration, + displayController, + dragEventListener, + transitions, + interactionJankMonitor, + handler)) : new FluidResizeTaskPositioner( taskOrganizer, transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt new file mode 100644 index 000000000000..8dc921c986ce --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -0,0 +1,293 @@ +/* + * 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.graphics.PointF +import android.graphics.Rect +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.view.Choreographer +import android.view.Surface +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.internal.jank.Cuj +import com.android.internal.jank.InteractionJankMonitor +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.Transitions +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +/** + * A task positioner that also takes into account resizing a + * [com.android.wm.shell.windowdecor.ResizeVeil] and dragging move across multiple displays. + * - If the drag is resizing the task, we resize the veil instead. + * - If the drag is repositioning, we consider multi-display topology if needed, and update in the + * typical manner. + */ +class MultiDisplayVeiledResizeTaskPositioner( + private val taskOrganizer: ShellTaskOrganizer, + private val desktopWindowDecoration: DesktopModeWindowDecoration, + private val displayController: DisplayController, + dragEventListener: DragPositioningCallbackUtility.DragEventListener, + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val transitions: Transitions, + private val interactionJankMonitor: InteractionJankMonitor, + @ShellMainThread private val handler: Handler, +) : TaskPositioner, Transitions.TransitionHandler { + private val dragEventListeners = + mutableListOf<DragPositioningCallbackUtility.DragEventListener>() + private val stableBounds = Rect() + private val taskBoundsAtDragStart = Rect() + private val repositionStartPoint = PointF() + private val repositionTaskBounds = Rect() + private val isResizing: Boolean + get() = + (ctrlType and DragPositioningCallback.CTRL_TYPE_TOP) != 0 || + (ctrlType and DragPositioningCallback.CTRL_TYPE_BOTTOM) != 0 || + (ctrlType and DragPositioningCallback.CTRL_TYPE_LEFT) != 0 || + (ctrlType and DragPositioningCallback.CTRL_TYPE_RIGHT) != 0 + + @DragPositioningCallback.CtrlType private var ctrlType = 0 + private var isResizingOrAnimatingResize = false + @Surface.Rotation private var rotation = 0 + + constructor( + taskOrganizer: ShellTaskOrganizer, + windowDecoration: DesktopModeWindowDecoration, + displayController: DisplayController, + dragEventListener: DragPositioningCallbackUtility.DragEventListener, + transitions: Transitions, + interactionJankMonitor: InteractionJankMonitor, + @ShellMainThread handler: Handler, + ) : this( + taskOrganizer, + windowDecoration, + displayController, + dragEventListener, + Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() }, + transitions, + interactionJankMonitor, + handler, + ) + + init { + dragEventListeners.add(dragEventListener) + } + + override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { + this.ctrlType = ctrlType + taskBoundsAtDragStart.set( + desktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.bounds + ) + repositionStartPoint[x] = y + if (isResizing) { + // Capture CUJ for re-sizing window in DW mode. + interactionJankMonitor.begin( + createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + ) + if (!desktopWindowDecoration.mHasGlobalFocus) { + val wct = WindowContainerTransaction() + wct.reorder( + desktopWindowDecoration.mTaskInfo.token, + /* onTop= */ true, + /* includingParents= */ true, + ) + taskOrganizer.applyTransaction(wct) + } + } + for (dragEventListener in dragEventListeners) { + dragEventListener.onDragStart(desktopWindowDecoration.mTaskInfo.taskId) + } + repositionTaskBounds.set(taskBoundsAtDragStart) + val rotation = + desktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.displayRotation + if (stableBounds.isEmpty || this.rotation != rotation) { + this.rotation = rotation + displayController + .getDisplayLayout(desktopWindowDecoration.mDisplay.displayId)!! + .getStableBounds(stableBounds) + } + return Rect(repositionTaskBounds) + } + + override fun onDragPositioningMove(displayId: Int, x: Float, y: Float): Rect { + check(Looper.myLooper() == handler.looper) { + "This method must run on the shell main thread." + } + val delta = DragPositioningCallbackUtility.calculateDelta(x, y, repositionStartPoint) + if ( + isResizing && + DragPositioningCallbackUtility.changeBounds( + ctrlType, + repositionTaskBounds, + taskBoundsAtDragStart, + stableBounds, + delta, + displayController, + desktopWindowDecoration, + ) + ) { + if (!isResizingOrAnimatingResize) { + for (dragEventListener in dragEventListeners) { + dragEventListener.onDragMove(desktopWindowDecoration.mTaskInfo.taskId) + } + desktopWindowDecoration.showResizeVeil(repositionTaskBounds) + isResizingOrAnimatingResize = true + } else { + desktopWindowDecoration.updateResizeVeil(repositionTaskBounds) + } + } else if (ctrlType == DragPositioningCallback.CTRL_TYPE_UNDEFINED) { + // Begin window drag CUJ instrumentation only when drag position moves. + interactionJankMonitor.begin( + createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) + ) + val t = transactionSupplier.get() + DragPositioningCallbackUtility.setPositionOnDrag( + desktopWindowDecoration, + repositionTaskBounds, + taskBoundsAtDragStart, + repositionStartPoint, + t, + x, + y, + ) + t.setFrameTimeline(Choreographer.getInstance().vsyncId) + t.apply() + } + return Rect(repositionTaskBounds) + } + + override fun onDragPositioningEnd(displayId: Int, x: Float, y: Float): Rect { + val delta = DragPositioningCallbackUtility.calculateDelta(x, y, repositionStartPoint) + if (isResizing) { + if (taskBoundsAtDragStart != repositionTaskBounds) { + DragPositioningCallbackUtility.changeBounds( + ctrlType, + repositionTaskBounds, + taskBoundsAtDragStart, + stableBounds, + delta, + displayController, + desktopWindowDecoration, + ) + desktopWindowDecoration.updateResizeVeil(repositionTaskBounds) + val wct = WindowContainerTransaction() + wct.setBounds(desktopWindowDecoration.mTaskInfo.token, repositionTaskBounds) + transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, this) + } else { + // If bounds haven't changed, perform necessary veil reset here as startAnimation + // won't be called. + resetVeilIfVisible() + } + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + } else { + DragPositioningCallbackUtility.updateTaskBounds( + repositionTaskBounds, + taskBoundsAtDragStart, + repositionStartPoint, + x, + y, + ) + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) + } + + ctrlType = DragPositioningCallback.CTRL_TYPE_UNDEFINED + taskBoundsAtDragStart.setEmpty() + repositionStartPoint[0f] = 0f + return Rect(repositionTaskBounds) + } + + private fun resetVeilIfVisible() { + if (isResizingOrAnimatingResize) { + desktopWindowDecoration.hideResizeVeil() + isResizingOrAnimatingResize = false + } + } + + private fun createLongTimeoutJankConfigBuilder(@Cuj.CujType cujType: Int) = + InteractionJankMonitor.Configuration.Builder.withSurface( + cujType, + desktopWindowDecoration.mContext, + desktopWindowDecoration.mTaskSurface, + handler, + ) + .setTimeout(LONG_CUJ_TIMEOUT_MS) + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + for (change in info.changes) { + val sc = change.leash + val endBounds = change.endAbsBounds + val endPosition = change.endRelOffset + startTransaction + .setWindowCrop(sc, endBounds.width(), endBounds.height()) + .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat()) + finishTransaction + .setWindowCrop(sc, endBounds.width(), endBounds.height()) + .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat()) + } + + startTransaction.apply() + resetVeilIfVisible() + ctrlType = DragPositioningCallback.CTRL_TYPE_UNDEFINED + finishCallback.onTransitionFinished(null /* wct */) + isResizingOrAnimatingResize = false + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) + return true + } + + /** + * We should never reach this as this handler's transitions are only started from shell + * explicitly. + */ + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? { + return null + } + + override fun isResizingOrAnimating() = isResizingOrAnimatingResize + + override fun addDragEventListener( + dragEventListener: DragPositioningCallbackUtility.DragEventListener + ) { + dragEventListeners.add(dragEventListener) + } + + override fun removeDragEventListener( + dragEventListener: DragPositioningCallbackUtility.DragEventListener + ) { + dragEventListeners.remove(dragEventListener) + } + + companion object { + // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid + // timing out in the middle of a resize or drag action. + private val LONG_CUJ_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(/* duration= */ 10L) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index e011cc08903b..d2c79d76e6c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -53,7 +53,12 @@ import java.util.function.Supplier; * {@link com.android.wm.shell.windowdecor.ResizeVeil}. * If the drag is resizing the task, we resize the veil instead. * If the drag is repositioning, we update in the typical manner. + * <p> + * @deprecated This class will be replaced by + * {@link com.android.wm.shell.windowdecor.MultiDisplayVeiledResizeTaskPositioner}. + * TODO(b/383632995): Remove this class after MultiDisplayVeiledResizeTaskPositioner is launched. */ +@Deprecated public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.TransitionHandler { // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid // timing out in the middle of a resize or drag action. diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppsWithBackNavigationLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppsWithBackNavigationLandscape.kt new file mode 100644 index 000000000000..0feb13d9dded --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppsWithBackNavigationLandscape.kt @@ -0,0 +1,51 @@ +/* + * 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.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.scenarios.CloseAllAppsWithBackNavigation +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppsWithBackNavigationLandscape : CloseAllAppsWithBackNavigation( + Rotation.ROTATION_90 +) { + // TODO(b/390043833): Add verifications that TO_BACK transition is captured when the back + // navigation button is pressed. + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppsWithBackNavigationPortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppsWithBackNavigationPortrait.kt new file mode 100644 index 000000000000..3b77ea5e5264 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/CloseAllAppsWithBackNavigationPortrait.kt @@ -0,0 +1,48 @@ +/* + * 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.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP +import com.android.wm.shell.scenarios.CloseAllAppsWithBackNavigation +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class CloseAllAppsWithBackNavigationPortrait : CloseAllAppsWithBackNavigation() { + // TODO(b/390043833): Add verifications that TO_BACK transition is captured when the back + // navigation button is pressed. + @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"]) + @Test + override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(CLOSE_APP) + .use(CLOSE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragExistingWindowsTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragExistingWindowsTest.kt new file mode 100644 index 000000000000..2b26bbfb68cb --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopWithDragExistingWindowsTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.EnterDesktopWithDragExistingWindows +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [EnterDesktopWithDragExistingWindows]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class EnterDesktopWithDragExistingWindowsTest : EnterDesktopWithDragExistingWindows() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithBackNavigation.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithBackNavigation.kt new file mode 100644 index 000000000000..319df49cdc03 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/CloseAllAppsWithBackNavigation.kt @@ -0,0 +1,73 @@ +/* + * 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.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.MailAppHelper +import com.android.server.wm.flicker.helpers.NonResizeableAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Base Test Class") +abstract class CloseAllAppsWithBackNavigation(val rotation: Rotation = Rotation.ROTATION_0) { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + Assume.assumeTrue(Flags.enableDesktopWindowingBackNavigation()) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + testApp.enterDesktopMode(wmHelper, device) + mailApp.launchViaIntent(wmHelper) + nonResizeableApp.launchViaIntent(wmHelper) + } + + @Test + open fun closeAllAppsInDesktop() { + nonResizeableApp.closeDesktopApp(wmHelper, device, usingBackNavigation = true) + mailApp.closeDesktopApp(wmHelper, device, usingBackNavigation = true) + testApp.closeDesktopApp(wmHelper, device, usingBackNavigation = true) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenuExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenuExistingWindows.kt new file mode 100644 index 000000000000..2de0830dedb5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithAppHandleMenuExistingWindows.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.platform.test.annotations.Postsubmit +import android.app.Instrumentation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.window.flags.Flags +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class EnterDesktopWithAppHandleMenuExistingWindows { + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val imeApp = ImeAppHelper(instrumentation) + private val newTaskApp = NewTasksAppHelper(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopMode(wmHelper, device) + imeApp.launchViaIntent(wmHelper) + newTaskApp.launchViaIntent(wmHelper) + testApp.launchViaIntent(wmHelper) + testApp.exitDesktopWithDragToTopDragZone(wmHelper, device) + } + + @Test + open fun reenterDesktopWithAppHandleMenu() { + testApp.enterDesktopModeFromAppHandleMenu(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + newTaskApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt new file mode 100644 index 000000000000..814478af67c1 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import com.android.server.wm.flicker.helpers.ImeAppHelper +import com.android.server.wm.flicker.helpers.NewTasksAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +@Ignore("Test Base Class") +abstract class EnterDesktopWithDragExistingWindows +constructor( + val rotation: Rotation = Rotation.ROTATION_0, + isResizeable: Boolean = true, + isLandscapeApp: Boolean = true, +) : DesktopScenarioCustomAppTestBase(isResizeable, isLandscapeApp) { + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + private val imeApp = ImeAppHelper(instrumentation) + private val newTaskApp = NewTasksAppHelper(instrumentation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + ChangeDisplayOrientationRule.setRotation(rotation) + tapl.enableTransientTaskbar(false) + + testApp.enterDesktopMode(wmHelper, device) + imeApp.launchViaIntent(wmHelper) + newTaskApp.launchViaIntent(wmHelper) + testApp.launchViaIntent(wmHelper) + testApp.exitDesktopWithDragToTopDragZone(wmHelper, device) + } + + @Test + open fun reenterDesktopWithDrag() { + // By default this method uses drag to desktop + testApp.enterDesktopMode(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + newTaskApp.exit(wmHelper) + imeApp.exit(wmHelper) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenUnlimitedApps.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenUnlimitedApps.kt index 367c4a437018..16a01d570575 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenUnlimitedApps.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenUnlimitedApps.kt @@ -17,6 +17,8 @@ package com.android.wm.shell.scenarios import android.app.Instrumentation +import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.tools.traces.parsers.WindowManagerStateHelper import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -43,7 +45,7 @@ abstract class OpenUnlimitedApps() private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) + private val mailApp = MailAppHelper(instrumentation) private val maxNum = DesktopModeStatus.getMaxTaskLimit(instrumentation.context) @@ -61,7 +63,12 @@ abstract class OpenUnlimitedApps() // Launch new [openTaskNum] tasks. for (i in 1..openTaskNum) { - mailApp.launchViaIntent(wmHelper) + mailApp.launchViaIntent( + wmHelper, + mailApp.openAppIntent.apply { + addFlags(FLAG_ACTIVITY_MULTIPLE_TASK or FLAG_ACTIVITY_NEW_TASK) + } + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt index ba4654260864..31d89f92f744 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/CopyContentInSplit.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -61,7 +62,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt index d774a31220da..1af6cac39085 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByDivider.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -74,7 +75,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt index 5aa161911a9a..8ad8c7bd7a7f 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DismissSplitScreenByGoHome.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -60,7 +61,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt index 668f3678bb38..da0ace472153 100644 --- a/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/scenarios/src/com/android/wm/shell/scenarios/DragDividerToResize.kt @@ -24,6 +24,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.RecentTasksUtils import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.After import org.junit.Before @@ -61,7 +62,6 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @After fun teardown() { - primaryApp.exit(wmHelper) - secondaryApp.exit(wmHelper) + RecentTasksUtils.clearAllVisibleRecentTasks(instrumentation) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt new file mode 100644 index 000000000000..aa262f9dfd6a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/RecentTasksUtils.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.utils + +import android.app.Instrumentation + +object RecentTasksUtils { + fun clearAllVisibleRecentTasks(instrumentation: Instrumentation) { + instrumentation.uiAutomation.executeShellCommand( + "dumpsys activity service SystemUIService WMShell recents clearAll" + ) + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 95ed8b4d4511..2b986d184c20 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -601,7 +601,32 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + ) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(4) + // Expect order to be from bottom: home, wallpaperIntent, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + wct.assertReorderAt(index = 2, task1) + wct.assertReorderAt(index = 3, task2) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) @@ -1775,8 +1800,35 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags( + FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) + fun moveToNextDisplay_wallpaperOnSystemUser_reorderWallpaperToBack() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Add a task and a wallpaper + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val wallpaperChange = + hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() } + assertNotNull(wallpaperChange) + assertThat(wallpaperChange.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + } + } + + @Test @EnableFlags(FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) - fun moveToNextDisplay_removeWallpaper() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + fun moveToNextDisplay_wallpaperNotOnSystemUser_removeWallpaper() { // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index c42f6c35bcb0..aef44a40fa0f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -16,14 +16,10 @@ package com.android.wm.shell.pip2.phone; -import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; -import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; - import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; import static org.mockito.kotlin.MatchersKt.eq; @@ -124,12 +120,13 @@ public class PipSchedulerTest { .setCallsite("PipSchedulerTest") .build(); when(mMockPipTransitionState.getPinnedTaskLeash()).thenReturn(testLeash); + // PiP is in a valid state by default. + when(mMockPipTransitionState.isInPip()).thenReturn(true); } @Test public void scheduleExitPipViaExpand_nullTaskToken_noop() { setNullPipTaskToken(); - when(mMockPipTransitionState.isInPip()).thenReturn(true); mPipScheduler.scheduleExitPipViaExpand(); @@ -137,14 +134,12 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, never()) - .startExitTransition(eq(TRANSIT_EXIT_PIP), any(), isNull()); + verify(mMockPipTransitionController, never()).startExpandTransition(any()); } @Test public void scheduleExitPipViaExpand_exitTransitionCalled() { setMockPipTaskToken(); - when(mMockPipTransitionState.isInPip()).thenReturn(true); mPipScheduler.scheduleExitPipViaExpand(); @@ -152,23 +147,21 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, times(1)) - .startExitTransition(eq(TRANSIT_EXIT_PIP), any(), isNull()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any()); } @Test public void removePipAfterAnimation() { setMockPipTaskToken(); - when(mMockPipTransitionState.isInPip()).thenReturn(true); - mPipScheduler.scheduleRemovePip(); + mPipScheduler.scheduleRemovePip(true /* withFadeout */); verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); verify(mMockPipTransitionController, times(1)) - .startExitTransition(eq(TRANSIT_REMOVE_PIP), any(), isNull()); + .startRemoveTransition(true /* withFadeout */); } @Test @@ -192,7 +185,6 @@ public class PipSchedulerTest { @Test public void scheduleAnimateResizePip_boundsConfig_setsConfigAtEnd() { setMockPipTaskToken(); - when(mMockPipTransitionState.isInPip()).thenReturn(true); mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true); @@ -233,7 +225,6 @@ public class PipSchedulerTest { @Test public void scheduleAnimateResizePip_resizeTransition() { setMockPipTaskToken(); - when(mMockPipTransitionState.isInPip()).thenReturn(true); mPipScheduler.scheduleAnimateResizePip(TEST_BOUNDS, true, TEST_RESIZE_DURATION); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java index 571ae93e1aec..fa9b5903bc3c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTransitionStateTest.java @@ -110,4 +110,22 @@ public class PipTransitionStateTest extends ShellTestCase { mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener); } + + @Test + public void testBoundsChangeState_notInPip() { + Bundle extra = new Bundle(); + extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable); + + mPipTransitionState.setState(PipTransitionState.UNDEFINED); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + Assert.assertEquals(PipTransitionState.UNDEFINED, mPipTransitionState.getState()); + + mPipTransitionState.setState(PipTransitionState.ENTERING_PIP, extra); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + Assert.assertEquals(PipTransitionState.ENTERING_PIP, mPipTransitionState.getState()); + + mPipTransitionState.setState(PipTransitionState.EXITING_PIP); + mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra); + Assert.assertEquals(PipTransitionState.EXITING_PIP, mPipTransitionState.getState()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 7e5d6ce38c5a..28f4ea0c7ada 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -22,13 +22,13 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; -import static com.android.launcher3.Flags.FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE; import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FREEFORM; import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_FULLSCREEN; import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -65,8 +65,8 @@ import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; @@ -253,7 +253,6 @@ public class RecentTasksControllerTest extends ShellTestCase { t3.taskId, -1); } - @EnableFlags(FLAG_ENABLE_USE_TOP_VISIBLE_ACTIVITY_FOR_EXCLUDE_FROM_RECENT_TASK) @Test public void testGetRecentTasks_removesDesktopWallpaperActivity() { RecentTaskInfo t1 = makeTaskInfo(1); @@ -753,15 +752,9 @@ public class RecentTasksControllerTest extends ShellTestCase { /** * Helper to set the raw task list on the controller. */ - private ArrayList<RecentTaskInfo> setRawList( - RecentTaskInfo... tasks) { - ArrayList<RecentTaskInfo> rawList = new ArrayList<>(); - for (RecentTaskInfo task : tasks) { - rawList.add(task); - } - doReturn(rawList).when(mActivityTaskManager).getRecentTasks(anyInt(), anyInt(), + private void setRawList(RecentTaskInfo... tasks) { + doReturn(Arrays.asList(tasks)).when(mActivityTaskManager).getRecentTasks(anyInt(), anyInt(), anyInt()); - return rawList; } /** @@ -794,8 +787,9 @@ public class RecentTasksControllerTest extends ShellTestCase { assertNull(pair.getSplitBounds()); } } - assertTrue("Expected: " + Arrays.toString(expectedTaskIds) + assertArrayEquals("Expected: " + Arrays.toString(expectedTaskIds) + " Received: " + Arrays.toString(flattenedTaskIds), - Arrays.equals(flattenedTaskIds, expectedTaskIds)); + flattenedTaskIds, + expectedTaskIds); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index 2d0ea5fdc884..7a88ace3f85f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -17,6 +17,7 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED; @@ -32,6 +33,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.atLeastOnce; @@ -50,9 +53,11 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.view.SurfaceControl; import android.window.RemoteTransition; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.test.annotation.UiThreadTest; @@ -84,6 +89,7 @@ import com.android.wm.shell.transition.Transitions; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -425,6 +431,30 @@ public class StageCoordinatorTests extends ShellTestCase { .startFullscreenTransition(any(), any()); } + + @Test + public void startTask_ensureWindowingModeCleared() { + SplitScreenTransitions splitScreenTransitions = + spy(mStageCoordinator.getSplitTransitions()); + mStageCoordinator.setSplitTransitions(splitScreenTransitions); + ArgumentCaptor<WindowContainerTransaction> wctCaptor = + ArgumentCaptor.forClass(WindowContainerTransaction.class); + int taskId = 18; + IBinder binder = mock(IBinder.class); + ActivityManager.RunningTaskInfo rti = mock(ActivityManager.RunningTaskInfo.class); + WindowContainerToken mockToken = mock(WindowContainerToken.class); + when(mockToken.asBinder()).thenReturn(binder); + when(rti.getToken()).thenReturn(mockToken); + when(mTaskOrganizer.getRunningTaskInfo(taskId)).thenReturn(rti); + mStageCoordinator.startTask(taskId, SPLIT_POSITION_TOP_OR_LEFT, null /*options*/, + null, SPLIT_INDEX_UNDEFINED); + verify(splitScreenTransitions).startEnterTransition(anyInt(), + wctCaptor.capture(), any(), any(), anyInt(), anyBoolean()); + + int windowingMode = wctCaptor.getValue().getChanges().get(binder).getWindowingMode(); + assertEquals(windowingMode, WINDOWING_MODE_UNDEFINED); + } + private Transitions createTestTransitions() { ShellInit shellInit = new ShellInit(mMainExecutor); final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java index 015ea20767e9..74c2f0e6753a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java @@ -17,7 +17,9 @@ package com.android.wm.shell.transition; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; @@ -127,19 +129,136 @@ public class FocusTransitionObserverTest extends ShellTestCase { true /* isFocusedOnDisplay */, false /* isFocusedGlobally */); } + @Test + public void testTaskFocusSwitch() throws RemoteException { + final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + + // Open 2 tasks on the default display. + TransitionInfo info = mock(TransitionInfo.class); + final List<TransitionInfo.Change> changes = new ArrayList<>(); + setupTaskChange(changes, 1 /* taskId */, TRANSIT_OPEN, + DEFAULT_DISPLAY, true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, never()).onFocusedDisplayChanged(anyInt()); + verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + clearInvocations(mListener); + changes.clear(); + + setupTaskChange(changes, 2 /* taskId */, TRANSIT_OPEN, + DEFAULT_DISPLAY, true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */, + false /* isFocusedOnDisplay */, false /* isFocusedGlobally */); + verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + clearInvocations(mListener); + changes.clear(); + + // Moving a task to front. + changes.clear(); + setupTaskChange(changes, 1 /* taskId */, TRANSIT_TO_FRONT, + DEFAULT_DISPLAY, true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */, + false /* isFocusedOnDisplay */, false /* isFocusedGlobally */); + } + + + @Test + public void testTaskMoveToAnotherDisplay() throws RemoteException { + final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class); + + // First, open a task on the default display. + TransitionInfo info = mock(TransitionInfo.class); + final List<TransitionInfo.Change> changes = new ArrayList<>(); + setupTaskChange(changes, 1 /* taskId */, TRANSIT_OPEN, + DEFAULT_DISPLAY, true /* focused */); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, never()).onFocusedDisplayChanged(anyInt()); + verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + clearInvocations(mListener); + changes.clear(); + + // Open 2 tasks on the secondary display. + setupTaskChange(changes, 2 /* taskId */, TRANSIT_OPEN, + SECONDARY_DISPLAY_ID, true /* focused */); + setupDisplayToTopChange(changes, SECONDARY_DISPLAY_ID); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, times(1)) + .onFocusedDisplayChanged(SECONDARY_DISPLAY_ID); + verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */, + true /* isFocusedOnDisplay */, false /* isFocusedGlobally */); + verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + clearInvocations(mListener); + changes.clear(); + + setupTaskChange(changes, 3 /* taskId */, TRANSIT_OPEN, + SECONDARY_DISPLAY_ID, true /* focused */); + setupDisplayToTopChange(changes, SECONDARY_DISPLAY_ID); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */, + false /* isFocusedOnDisplay */, false /* isFocusedGlobally */); + verify(mListener, times(1)).onFocusedTaskChanged(3 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + clearInvocations(mListener); + changes.clear(); + + // Move focused task in the secondary display to the default display + setupTaskChange(changes, 3 /* taskId */, TRANSIT_CHANGE, + SECONDARY_DISPLAY_ID, DEFAULT_DISPLAY, true /* focused */); + setupTaskChange(changes, 2 /* taskId */, TRANSIT_TO_FRONT, + SECONDARY_DISPLAY_ID, true /* focused */); + setupDisplayToTopChange(changes, DEFAULT_DISPLAY); + when(info.getChanges()).thenReturn(changes); + mFocusTransitionObserver.updateFocusState(info); + mShellExecutor.flushAll(); + verify(mListener, times(1)).onFocusedTaskChanged(1 /* taskId */, + false /* isFocusedOnDisplay */, false /* isFocusedGlobally */); + verify(mListener, times(1)).onFocusedTaskChanged(2 /* taskId */, + true /* isFocusedOnDisplay */, false /* isFocusedGlobally */); + verify(mListener, times(1)).onFocusedTaskChanged(3 /* taskId */, + true /* isFocusedOnDisplay */, true /* isFocusedGlobally */); + clearInvocations(mListener); + } + private void setupTaskChange(List<TransitionInfo.Change> changes, int taskId, @TransitionMode int mode, int displayId, boolean focused) { + setupTaskChange(changes, taskId, mode, displayId, displayId, focused); + } + + private void setupTaskChange(List<TransitionInfo.Change> changes, int taskId, + @TransitionMode int mode, int startDisplayId, int endDisplayId, boolean focused) { TransitionInfo.Change change = mock(TransitionInfo.Change.class); RunningTaskInfo taskInfo = mock(RunningTaskInfo.class); taskInfo.taskId = taskId; taskInfo.isFocused = focused; when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused); - taskInfo.displayId = displayId; + taskInfo.displayId = endDisplayId; + when(change.getStartDisplayId()).thenReturn(startDisplayId); + when(change.getEndDisplayId()).thenReturn(endDisplayId); when(change.getTaskInfo()).thenReturn(taskInfo); when(change.getMode()).thenReturn(mode); changes.add(change); } + private void setupDisplayToTopChange(List<TransitionInfo.Change> changes, int displayId) { TransitionInfo.Change change = mock(TransitionInfo.Change.class); when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(true); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index dd645fd968e4..0a19be4eb959 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -1781,6 +1781,7 @@ public class ShellTransitionTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); taskInfo.configuration.windowConfiguration.setActivityType(activityType); taskInfo.token = mock(WindowContainerToken.class); + taskInfo.baseIntent = mock(Intent.class); return taskInfo; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index 8af8285d031c..b44af4733fd2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -182,6 +182,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { spyContext.addMockSystemService(InputManager::class.java, mockInputManager) desktopModeEventLogger = mock<DesktopModeEventLogger>() whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) + whenever(mockDisplayController.getDisplayContext(any())).thenReturn(spyContext) whenever(mockDesktopUserRepositories.getProfile(anyInt())) .thenReturn(mockDesktopRepository) desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt new file mode 100644 index 000000000000..f179cac32244 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -0,0 +1,611 @@ +/* + * 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.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.graphics.Rect +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.Surface.ROTATION_0 +import android.view.Surface.ROTATION_270 +import android.view.Surface.ROTATION_90 +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.TransitionInfo +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.android.internal.jank.InteractionJankMonitor +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback +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.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import java.util.function.Supplier +import junit.framework.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.argThat +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** + * Tests for [MultiDisplayVeiledResizeTaskPositioner]. + * + * Build/Install/Run: atest WMShellUnitTests:MultiDisplayVeiledResizeTaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { + + @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration + @Mock + private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener + + @Mock private lateinit var taskToken: WindowContainerToken + @Mock private lateinit var taskBinder: IBinder + + @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock private lateinit var mockDisplay: Display + @Mock private lateinit var mockTransactionFactory: Supplier<SurfaceControl.Transaction> + @Mock private lateinit var mockTransaction: SurfaceControl.Transaction + @Mock private lateinit var mockTransitionBinder: IBinder + @Mock private lateinit var mockTransitionInfo: TransitionInfo + @Mock private lateinit var mockFinishCallback: TransitionFinishCallback + @Mock private lateinit var mockTransitions: Transitions + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + private val mainHandler = Handler(Looper.getMainLooper()) + + private lateinit var taskPositioner: MultiDisplayVeiledResizeTaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + mockDesktopWindowDecoration.mDisplay = mockDisplay + mockDesktopWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) + whenever(taskToken.asBinder()).thenReturn(taskBinder) + whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + if ( + mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration + .displayRotation == ROTATION_90 || + mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration + .displayRotation == ROTATION_270 + ) { + (i.arguments.first() as Rect).set(STABLE_BOUNDS_LANDSCAPE) + } else { + (i.arguments.first() as Rect).set(STABLE_BOUNDS_PORTRAIT) + } + } + `when`(mockTransactionFactory.get()).thenReturn(mockTransaction) + mockDesktopWindowDecoration.mTaskInfo = + ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = MIN_WIDTH + minHeight = MIN_HEIGHT + defaultMinSize = DEFAULT_MIN + 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 + whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + + taskPositioner = + MultiDisplayVeiledResizeTaskPositioner( + mockShellTaskOrganizer, + mockDesktopWindowDecoration, + mockDisplayController, + mockDragEventListener, + mockTransactionFactory, + mockTransitions, + mockInteractionJankMonitor, + mainHandler, + ) + } + + @Test + fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + verify(mockDesktopWindowDecoration, never()).showResizeVeil(STARTING_BOUNDS) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + verify(mockTransitions, never()) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == STARTING_BOUNDS + } + }, + eq(taskPositioner), + ) + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() + } + + @Test + fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() + 60, + STARTING_BOUNDS.top.toFloat() + 100, + ) + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.left += 60 + rectAfterMove.right += 60 + rectAfterMove.top += 100 + rectAfterMove.bottom += 100 + verify(mockTransaction) + .setPosition(any(), eq(rectAfterMove.left.toFloat()), eq(rectAfterMove.top.toFloat())) + + val endBounds = + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() + 70, + STARTING_BOUNDS.top.toFloat() + 20, + ) + val rectAfterEnd = Rect(STARTING_BOUNDS) + rectAfterEnd.left += 70 + rectAfterEnd.right += 70 + rectAfterEnd.top += 20 + rectAfterEnd.bottom += 20 + + verify(mockDesktopWindowDecoration, never()).showResizeVeil(any()) + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() + Assert.assertEquals(rectAfterEnd, endBounds) + } + + @Test + fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DISPLAY_ID, + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.right.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10, + ) + + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.right += 10 + rectAfterMove.top += 10 + verify(mockDesktopWindowDecoration).showResizeVeil(rectAfterMove) + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterMove + } + } + ) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.right.toFloat() + 20, + STARTING_BOUNDS.top.toFloat() + 20, + ) + val rectAfterEnd = Rect(rectAfterMove) + rectAfterEnd.right += 10 + rectAfterEnd.top += 10 + verify(mockDesktopWindowDecoration).updateResizeVeil(any()) + verify(mockTransitions) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }, + eq(taskPositioner), + ) + } + + @Test + fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread { + taskPositioner.onDragPositioningStart( + DISPLAY_ID, + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10, + ) + + verify(mockTransitions, never()) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == STARTING_BOUNDS + } + }, + eq(taskPositioner), + ) + + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0) + } + } + ) + } + + @Test + fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + val newX = STARTING_BOUNDS.left.toFloat() + 5 + val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1 + taskPositioner.onDragPositioningMove(DISPLAY_ID, newX, newY) + + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) + + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0) + } + } + ) + } + + @Test + fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread { + mockDesktopWindowDecoration.mHasGlobalFocus = false + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // Verify task is reordered to top + verify(mockShellTaskOrganizer) + .applyTransaction( + argThat { wct -> + return@argThat wct.hierarchyOps.any { hierarchyOps -> + hierarchyOps.container == taskBinder && hierarchyOps.toTop + } + } + ) + } + + @Test + fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread { + mockDesktopWindowDecoration.mHasGlobalFocus = true + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // Verify task is not reordered to top + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.hierarchyOps.any { hierarchyOps -> + hierarchyOps.container == taskBinder && hierarchyOps.toTop + } + } + ) + } + + @Test + fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread { + mockDesktopWindowDecoration.mHasGlobalFocus = false + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // Verify task is not reordered to top since task is already brought to top before dragging + // begins + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.hierarchyOps.any { hierarchyOps -> + hierarchyOps.container == taskBinder && hierarchyOps.toTop + } + } + ) + } + + @Test + fun testDragResize_drag_updatesStableBoundsOnRotate() = runOnUiThread { + // Test landscape stable bounds + performDrag( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat(), + STARTING_BOUNDS.right.toFloat() + 2000, + STARTING_BOUNDS.bottom.toFloat() + 2000, + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + ) + val rectAfterDrag = Rect(STARTING_BOUNDS) + rectAfterDrag.right += 2000 + rectAfterDrag.bottom = STABLE_BOUNDS_LANDSCAPE.bottom + // First drag; we should fetch stable bounds. + verify(mockDisplayLayout, times(1)).getStableBounds(any()) + verify(mockTransitions) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterDrag + } + }, + eq(taskPositioner), + ) + // Drag back to starting bounds. + performDrag( + STARTING_BOUNDS.right.toFloat() + 2000, + STARTING_BOUNDS.bottom.toFloat(), + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat(), + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + ) + + // Display did not rotate; we should use previous stable bounds + verify(mockDisplayLayout, times(1)).getStableBounds(any()) + + // Rotate the screen to portrait + mockDesktopWindowDecoration.mTaskInfo.apply { + configuration.windowConfiguration.displayRotation = ROTATION_0 + } + // Test portrait stable bounds + performDrag( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat(), + STARTING_BOUNDS.right.toFloat() + 2000, + STARTING_BOUNDS.bottom.toFloat() + 2000, + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + ) + rectAfterDrag.right = STABLE_BOUNDS_PORTRAIT.right + rectAfterDrag.bottom = STARTING_BOUNDS.bottom + 2000 + + verify(mockTransitions) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterDrag + } + }, + eq(taskPositioner), + ) + // Display has rotated; we expect a new stable bounds. + verify(mockDisplayLayout, times(2)).getStableBounds(any()) + } + + @Test + fun testIsResizingOrAnimatingResizeSet() = runOnUiThread { + Assert.assertFalse(taskPositioner.isResizingOrAnimating) + + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() - 20, + STARTING_BOUNDS.top.toFloat() - 20, + ) + + // isResizingOrAnimating should be set to true after move during a resize + Assert.assertTrue(taskPositioner.isResizingOrAnimating) + verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID)) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // isResizingOrAnimating should be not be set till false until after transition animation + Assert.assertTrue(taskPositioner.isResizingOrAnimating) + } + + @Test + fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() = runOnUiThread { + performDrag( + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + STARTING_BOUNDS.left.toFloat() - 20, + STARTING_BOUNDS.top.toFloat() - 20, + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + ) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + mockTransaction, + mockTransaction, + mockFinishCallback, + ) + + // isResizingOrAnimating should be set to false until after transition successfully consumed + Assert.assertFalse(taskPositioner.isResizingOrAnimating) + } + + @Test + fun testStartAnimation_useEndRelOffset() = runOnUiThread { + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(Transaction::class.java) + val finishTransaction = mock(Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.leash).thenReturn(mock(SurfaceControl::class.java)) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop(any(), eq(bounds.width()), eq(bounds.height()))) + .thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop(any(), eq(bounds.width()), eq(bounds.height()))) + .thenReturn(finishTransaction) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + startTransaction, + finishTransaction, + mockFinishCallback, + ) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + + private fun performDrag(startX: Float, startY: Float, endX: Float, endY: Float, ctrlType: Int) { + taskPositioner.onDragPositioningStart(ctrlType, DISPLAY_ID, startX, startY) + taskPositioner.onDragPositioningMove(DISPLAY_ID, endX, endY) + + taskPositioner.onDragPositioningEnd(DISPLAY_ID, endX, endY) + } + + companion object { + private const val TASK_ID = 5 + private const val MIN_WIDTH = 10 + private const val MIN_HEIGHT = 10 + private const val DENSITY_DPI = 20 + private const val DEFAULT_MIN = 40 + private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private const val CAPTION_HEIGHT = 50 + private const val DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT = 10 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + private val STARTING_BOUNDS = Rect(100, 100, 200, 200) + private val STABLE_BOUNDS_LANDSCAPE = + Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top + CAPTION_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + ) + private val STABLE_BOUNDS_PORTRAIT = + Rect( + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.left + CAPTION_HEIGHT, + DISPLAY_BOUNDS.bottom, + DISPLAY_BOUNDS.right - NAVBAR_HEIGHT, + ) + private val VALID_DRAG_AREA = + Rect( + DISPLAY_BOUNDS.left - 100, + STABLE_BOUNDS_LANDSCAPE.top, + DISPLAY_BOUNDS.right - 100, + DISPLAY_BOUNDS.bottom - 100, + ) + } +} diff --git a/libs/androidfw/ApkParsing.cpp b/libs/androidfw/ApkParsing.cpp index 7eedfdb5c921..b80c8755f23e 100644 --- a/libs/androidfw/ApkParsing.cpp +++ b/libs/androidfw/ApkParsing.cpp @@ -33,7 +33,7 @@ const size_t LIB_SUFFIX_LEN = LIB_SUFFIX.size(); static const std::array<std::string_view, 2> abis = {"arm64-v8a", "x86_64"}; namespace android::util { -const char* ValidLibraryPathLastSlash(const char* fileName, bool suppress64Bit, bool debuggable) { +const char* ValidLibraryPathLastSlash(const char* fileName, bool suppress64Bit) { // Make sure the filename is at least to the minimum library name size. const size_t fileNameLen = strlen(fileName); static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN; @@ -66,14 +66,6 @@ const char* ValidLibraryPathLastSlash(const char* fileName, bool suppress64Bit, return nullptr; } - if (!debuggable) { - // Make sure the filename starts with lib and ends with ".so". - if (strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX.data(), LIB_SUFFIX_LEN) != 0 - || strncmp(lastSlash, LIB_PREFIX.data(), LIB_PREFIX_LEN) != 0) { - return nullptr; - } - } - // Don't include 64 bit versions if they are suppressed if (suppress64Bit && std::find(abis.begin(), abis.end(), std::string_view( fileName + APK_LIB_LEN, lastSlash - fileName - APK_LIB_LEN)) != abis.end()) { diff --git a/libs/androidfw/include/androidfw/ApkParsing.h b/libs/androidfw/include/androidfw/ApkParsing.h index 194eaae8e12a..b288e152f383 100644 --- a/libs/androidfw/include/androidfw/ApkParsing.h +++ b/libs/androidfw/include/androidfw/ApkParsing.h @@ -24,7 +24,7 @@ extern const size_t APK_LIB_LEN; namespace android::util { // Checks if filename is a valid library path and returns a pointer to the last slash in the path // if it is, nullptr otherwise -const char* ValidLibraryPathLastSlash(const char* filename, bool suppress64Bit, bool debuggable); +const char* ValidLibraryPathLastSlash(const char* filename, bool suppress64Bit); // Equivalent to android.os.FileUtils.isFilenameSafe bool isFilenameSafe(const char* filename); diff --git a/libs/androidfw/tests/ApkParsing_test.cpp b/libs/androidfw/tests/ApkParsing_test.cpp index ac1dc9b88463..f1f9d7166914 100644 --- a/libs/androidfw/tests/ApkParsing_test.cpp +++ b/libs/androidfw/tests/ApkParsing_test.cpp @@ -27,57 +27,45 @@ using ::testing::NotNull; namespace android { TEST(ApkParsingTest, ValidArm64Path) { const char* path = "lib/arm64-v8a/library.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, false); ASSERT_THAT(lastSlash, NotNull()); ASSERT_THAT(lastSlash, Eq(path + 13)); } TEST(ApkParsingTest, ValidArm64PathButSuppressed) { const char* path = "lib/arm64-v8a/library.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, true, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, true); ASSERT_THAT(lastSlash, IsNull()); } TEST(ApkParsingTest, ValidArm32Path) { const char* path = "lib/armeabi-v7a/library.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, false); ASSERT_THAT(lastSlash, NotNull()); ASSERT_THAT(lastSlash, Eq(path + 15)); } -TEST(ApkParsingTest, InvalidMustStartWithLib) { - const char* path = "lib/arm64-v8a/random.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); - ASSERT_THAT(lastSlash, IsNull()); -} - -TEST(ApkParsingTest, InvalidMustEndInSo) { - const char* path = "lib/arm64-v8a/library.txt"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); - ASSERT_THAT(lastSlash, IsNull()); -} - TEST(ApkParsingTest, InvalidCharacter) { const char* path = "lib/arm64-v8a/lib#.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, false); ASSERT_THAT(lastSlash, IsNull()); } TEST(ApkParsingTest, InvalidSubdirectories) { const char* path = "lib/arm64-v8a/anything/library.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, false); ASSERT_THAT(lastSlash, IsNull()); } TEST(ApkParsingTest, InvalidFileAtRoot) { const char* path = "lib/library.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, false); ASSERT_THAT(lastSlash, IsNull()); } TEST(ApkParsingTest, InvalidPrefix) { const char* path = "assets/libhello.so"; - auto lastSlash = util::ValidLibraryPathLastSlash(path, false, false); + auto lastSlash = util::ValidLibraryPathLastSlash(path, false); ASSERT_THAT(lastSlash, IsNull()); } }
\ No newline at end of file diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 5e71d3360f39..2851dd8b1003 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -42,6 +42,16 @@ flag { } flag { + name: "high_contrast_text_inner_text_color" + namespace: "accessibility" + description: "Render text color by modifying its brightness instead of defaulting to black and white" + bug: "384793956" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "hdr_10bit_plus" namespace: "core_graphics" description: "Use 10101010 and FP16 formats for HDR-UI when available" diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index e05c3d695463..008b693edf02 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -37,6 +37,9 @@ namespace flags { constexpr bool high_contrast_text_small_text_rect() { return false; } +constexpr bool high_contrast_text_inner_text_color() { + return false; +} } // namespace flags #endif @@ -126,7 +129,25 @@ public: // inner gDrawTextBlobMode = DrawTextBlobMode::HctInner; Paint innerPaint(paint); - simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint); + if (flags::high_contrast_text_inner_text_color()) { + // Preserve some color information while still ensuring sufficient contrast. + // Thus we increase the lightness to make the color stand out against a black + // background, and vice-versa. For grayscale, we retain some gray to indicate + // states like disabled or to distinguish links. + bool isGrayscale = abs(lab.a) < 1 && abs(lab.b) < 1; + if (isGrayscale) { + if (darken) { + lab.L = lab.L < 40 ? 0 : 20; + } else { + lab.L = lab.L > 60 ? 100 : 80; + } + } else { + lab.L = darken ? 20 : 90; + } + simplifyPaint(uirenderer::LabToSRGB(lab, SK_AlphaOPAQUE), &innerPaint); + } else { + simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint); + } innerPaint.setStyle(SkPaint::kFill_Style); canvas->drawGlyphs(glyphFunc, glyphCount, innerPaint, x, y, totalAdvance); gDrawTextBlobMode = DrawTextBlobMode::Normal; diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp index 69fe40c755ea..6ab8e4e0e2ab 100644 --- a/libs/hwui/renderthread/RenderThread.cpp +++ b/libs/hwui/renderthread/RenderThread.cpp @@ -323,6 +323,7 @@ void RenderThread::initGrContextOptions(GrContextOptions& options) { } void RenderThread::destroyRenderingContext() { + ATRACE_CALL(); mFunctorManager.onContextDestroyed(); if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) { if (mEglManager->hasEglContext()) { @@ -520,7 +521,10 @@ void RenderThread::preload() { // EGL driver is always preloaded only if HWUI renders with GL. if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) { if (Properties::earlyPreloadGlContext()) { - queue().post([this]() { requireGlContext(); }); + queue().post([this]() { + ATRACE_NAME("earlyPreloadGlContext"); + requireGlContext(); + }); } else { std::thread eglInitThread([]() { eglGetDisplay(EGL_DEFAULT_DISPLAY); }); eglInitThread.detach(); @@ -528,9 +532,6 @@ void RenderThread::preload() { } else { requireVkContext(); } - if (Properties::earlyPreloadGlContext()) { - queue().post([]() { GraphicBufferAllocator::getInstance(); }); - } HardwareBitmapUploader::initialize(); } |