diff options
Diffstat (limited to 'libs/WindowManager/Shell')
386 files changed, 21813 insertions, 4844 deletions
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 9aaef3b1f655..cdff5858c77d 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -38,6 +38,17 @@ filegroup { path: "src", } +// Sources that have no dependencies that can be used directly downstream of this library +filegroup { + name: "wm_shell_util-sources", + srcs: [ + "src/com/android/wm/shell/util/**/*.java", + "src/com/android/wm/shell/common/split/SplitScreenConstants.java" + ], + path: "src", +} + +// Aidls which can be used directly downstream of this library filegroup { name: "wm_shell-aidls", srcs: [ @@ -122,11 +133,12 @@ android_library { "kotlinx-coroutines-android", "kotlinx-coroutines-core", "iconloader_base", - "jsr330", "protolog-lib", "WindowManager-Shell-proto", + "dagger2", "jsr330", ], kotlincflags: ["-Xjvm-default=enable"], manifest: "AndroidManifest.xml", + plugins: ["dagger2-compiler"], } diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS deleted file mode 100644 index e2c67fd8f8d4..000000000000 --- a/libs/WindowManager/Shell/OWNERS +++ /dev/null @@ -1,4 +0,0 @@ -# sysui owners -hwwang@google.com -winsonc@google.com -madym@google.com diff --git a/libs/WindowManager/Shell/res/color/size_compat_background_ripple.xml b/libs/WindowManager/Shell/res/color/size_compat_background_ripple.xml new file mode 100644 index 000000000000..329e5b9b31a0 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/size_compat_background_ripple.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_500" android:lStar="35" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/split_divider_background.xml b/libs/WindowManager/Shell/res/color/split_divider_background.xml new file mode 100644 index 000000000000..329e5b9b31a0 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/split_divider_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_500" android:lStar="35" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/taskbar_background.xml b/libs/WindowManager/Shell/res/color/taskbar_background.xml new file mode 100644 index 000000000000..329e5b9b31a0 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/taskbar_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@android:color/system_neutral1_500" android:lStar="35" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/color/unfold_transition_background.xml b/libs/WindowManager/Shell/res/color/unfold_transition_background.xml new file mode 100644 index 000000000000..63289a3f75d9 --- /dev/null +++ b/libs/WindowManager/Shell/res/color/unfold_transition_background.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Matches taskbar color --> + <item android:color="@android:color/system_neutral2_500" android:lStar="35" /> +</selector> diff --git a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml index 8710fb8ac69b..96d2d7c954d8 100644 --- a/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml +++ b/libs/WindowManager/Shell/res/drawable/bubble_manage_btn_bg.xml @@ -18,7 +18,7 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid - android:color="@android:color/system_neutral1_900" + android:color="@android:color/system_neutral1_800" /> <corners android:radius="20dp" /> diff --git a/libs/WindowManager/Shell/res/drawable/compat_hint_bubble.xml b/libs/WindowManager/Shell/res/drawable/compat_hint_bubble.xml new file mode 100644 index 000000000000..26848b13a1bc --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/compat_hint_bubble.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@color/compat_controls_background"/> + <corners android:radius="@dimen/compat_hint_corner_radius"/> +</shape>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/compat_hint_point.xml b/libs/WindowManager/Shell/res/drawable/compat_hint_point.xml new file mode 100644 index 000000000000..0e0ca37aaf25 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/compat_hint_point.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/compat_hint_point_width" + android:height="8dp" + android:viewportWidth="10" + android:viewportHeight="8"> + <path + android:fillColor="@color/compat_controls_background" + android:pathData="M10,0 l-4.1875,6.6875 a1,1 0 0,1 -1.625,0 l-4.1875,-6.6875z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/pip_split.xml b/libs/WindowManager/Shell/res/drawable/pip_split.xml new file mode 100644 index 000000000000..2cfdf6ed259b --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_split.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/pip_expand_action_inner_size" + android:height="@dimen/pip_expand_action_inner_size" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#FFFFFF" + android:pathData="M20,18h-5V6h5V18z M22,18V6c0-1.1-0.9-2-2-2h-5c-1.1,0-2,0.9-2,2v12c0,1.1,0.9,2,2,2h5C21.1,20,22,19.1,22,18z M9,18H4V6h5 + V18z M11,18V6c0-1.1-0.9-2-2-2H4C2.9,4,2,4.9,2,6v12c0,1.1,0.9,2,2,2h5C10.1,20,11,19.1,11,18z" /> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml index 73a48d31a814..ab74e43472c3 100644 --- a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button.xml @@ -15,14 +15,21 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> <path - android:fillColor="#aa000000" - android:pathData="M0,12 a12,12 0 1,0 24,0 a12,12 0 1,0 -24,0" /> - <path - android:fillColor="@android:color/white" - android:pathData="M17.65,6.35c-1.63,-1.63 -3.94,-2.57 -6.48,-2.31c-3.67,0.37 -6.69,3.35 -7.1,7.02C3.52,15.91 7.27,20 12,20c3.19,0 5.93,-1.87 7.21,-4.57c0.31,-0.66 -0.16,-1.43 -0.89,-1.43h-0.01c-0.37,0 -0.72,0.2 -0.88,0.53c-1.13,2.43 -3.84,3.97 -6.81,3.32c-2.22,-0.49 -4.01,-2.3 -4.49,-4.52C5.31,9.44 8.26,6 12,6c1.66,0 3.14,0.69 4.22,1.78l-2.37,2.37C13.54,10.46 13.76,11 14.21,11H19c0.55,0 1,-0.45 1,-1V5.21c0,-0.45 -0.54,-0.67 -0.85,-0.35L17.65,6.35z"/> + android:fillColor="@color/compat_controls_background" + android:pathData="M0,24 a24,24 0 1,0 48,0 a24,24 0 1,0 -48,0" /> + <group + android:translateX="12" + android:translateY="12"> + <path + android:fillColor="@color/compat_controls_text" + android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/> + <path + android:fillColor="@color/compat_controls_text" + android:pathData="M20,13c0,-4.42 -3.58,-8 -8,-8c-0.06,0 -0.12,0.01 -0.18,0.01v0l1.09,-1.09L11.5,2.5L8,6l3.5,3.5l1.41,-1.41l-1.08,-1.08C11.89,7.01 11.95,7 12,7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02C16.95,20.44 20,17.08 20,13z"/> + </group> </vector> diff --git a/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml new file mode 100644 index 000000000000..95decff24ac4 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/size_compat_restart_button_ripple.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/size_compat_background_ripple"> + <item android:drawable="@drawable/size_compat_restart_button"/> +</ripple>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml index c09ae53746da..0cf6d73162d2 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_manage_button.xml @@ -17,13 +17,13 @@ <com.android.wm.shell.common.AlphaOptimizedButton xmlns:android="http://schemas.android.com/apk/res/android" style="@android:style/Widget.DeviceDefault.Button.Borderless" - android:id="@+id/settings_button" + android:id="@+id/manage_button" android:layout_gravity="start" android:layout_width="wrap_content" - android:layout_height="40dp" - android:layout_marginTop="8dp" - android:layout_marginLeft="16dp" - android:layout_marginBottom="8dp" + android:layout_height="@dimen/bubble_manage_button_height" + android:layout_marginStart="@dimen/bubble_manage_button_margin" + android:layout_marginTop="@dimen/bubble_manage_button_margin" + android:layout_marginBottom="@dimen/bubble_manage_button_margin" android:focusable="true" android:text="@string/manage_bubbles_text" android:textSize="@*android:dimen/text_size_body_2_material" diff --git a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml index f4b3aca33dd7..298ad3025b00 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_manage_menu.xml @@ -25,15 +25,15 @@ android:id="@+id/bubble_manage_menu_dismiss_container" android:background="@drawable/bubble_manage_menu_row" android:layout_width="match_parent" - android:layout_height="48dp" + android:layout_height="@dimen/bubble_menu_item_height" android:gravity="center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/bubble_menu_padding" + android:paddingEnd="@dimen/bubble_menu_padding" android:orientation="horizontal"> <ImageView - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/bubble_menu_icon_size" + android:layout_height="@dimen/bubble_menu_icon_size" android:src="@drawable/ic_remove_no_shadow" android:tint="@color/bubbles_icon_tint"/> @@ -50,15 +50,15 @@ android:id="@+id/bubble_manage_menu_dont_bubble_container" android:background="@drawable/bubble_manage_menu_row" android:layout_width="match_parent" - android:layout_height="48dp" + android:layout_height="@dimen/bubble_menu_item_height" android:gravity="center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/bubble_menu_padding" + android:paddingEnd="@dimen/bubble_menu_padding" android:orientation="horizontal"> <ImageView - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/bubble_menu_icon_size" + android:layout_height="@dimen/bubble_menu_icon_size" android:src="@drawable/bubble_ic_stop_bubble" android:tint="@color/bubbles_icon_tint"/> @@ -75,16 +75,16 @@ android:id="@+id/bubble_manage_menu_settings_container" android:background="@drawable/bubble_manage_menu_row" android:layout_width="match_parent" - android:layout_height="48dp" + android:layout_height="@dimen/bubble_menu_item_height" android:gravity="center_vertical" - android:paddingStart="16dp" - android:paddingEnd="16dp" + android:paddingStart="@dimen/bubble_menu_padding" + android:paddingEnd="@dimen/bubble_menu_padding" android:orientation="horizontal"> <ImageView android:id="@+id/bubble_manage_menu_settings_icon" - android:layout_width="24dp" - android:layout_height="24dp" + android:layout_width="@dimen/bubble_menu_icon_size" + android:layout_height="@dimen/bubble_menu_icon_size" android:src="@drawable/ic_remove_no_shadow"/> <TextView diff --git a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml index 544b731bb550..05b15060946d 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_overflow_view.xml @@ -32,7 +32,7 @@ android:id="@+id/bubble_view_name" android:textAppearance="@*android:style/TextAppearance.DeviceDefault.ListItem" android:textSize="13sp" - android:layout_width="wrap_content" + android:layout_width="@dimen/bubble_name_width" android:layout_height="wrap_content" android:maxLines="1" android:lines="2" diff --git a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml index fd4c3ba87026..87deb8b5a1fd 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_stack_user_education.xml @@ -21,7 +21,6 @@ android:layout_width="wrap_content" android:paddingTop="48dp" android:paddingBottom="48dp" - android:paddingStart="@dimen/bubble_stack_user_education_side_inset" android:paddingEnd="16dp" android:layout_marginEnd="24dp" android:orientation="vertical" diff --git a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml index c5c42fca323d..fafe40e924f5 100644 --- a/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubbles_manage_button_education.xml @@ -23,7 +23,6 @@ android:clickable="true" android:paddingTop="28dp" android:paddingBottom="16dp" - android:paddingStart="@dimen/bubble_expanded_view_padding" android:paddingEnd="48dp" android:layout_marginEnd="24dp" android:orientation="vertical" @@ -66,27 +65,21 @@ android:id="@+id/button_layout" android:orientation="horizontal" > - <com.android.wm.shell.common.AlphaOptimizedButton - style="@android:style/Widget.Material.Button.Borderless" - android:id="@+id/manage" - android:layout_gravity="start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:focusable="true" - android:clickable="false" - android:text="@string/manage_bubbles_text" - android:textColor="@android:color/system_neutral1_900" + <include + layout="@layout/bubble_manage_button" /> <com.android.wm.shell.common.AlphaOptimizedButton - style="@android:style/Widget.Material.Button.Borderless" + style="@android:style/Widget.DeviceDefault.Button.Borderless" android:id="@+id/got_it" android:layout_gravity="start" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="@dimen/bubble_manage_button_height" android:focusable="true" android:text="@string/bubbles_user_education_got_it" + android:textSize="@*android:dimen/text_size_body_2_material" android:textColor="@android:color/system_neutral1_900" + android:background="@drawable/bubble_manage_btn_bg" /> </LinearLayout> </LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml new file mode 100644 index 000000000000..bb48bf7b8b2c --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/compat_mode_hint.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2019 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:clipToPadding="false" + android:paddingEnd="@dimen/compat_hint_padding_end" + android:paddingBottom="8dp" + android:clickable="true"> + + <TextView + android:id="@+id/compat_mode_hint_text" + android:layout_width="188dp" + android:layout_height="wrap_content" + android:lineSpacingExtra="4sp" + android:background="@drawable/compat_hint_bubble" + android:padding="16dp" + android:textAlignment="viewStart" + android:textColor="@color/compat_controls_text" + android:textSize="14sp"/> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:src="@drawable/compat_hint_point" + android:paddingHorizontal="@dimen/compat_hint_corner_radius" + android:contentDescription="@null"/> + +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index cd3153145be3..dc1683475c48 100644 --- a/libs/WindowManager/Shell/res/layout/size_compat_ui.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -14,17 +14,24 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<com.android.wm.shell.sizecompatui.SizeCompatRestartButton +<com.android.wm.shell.compatui.CompatUILayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="bottom|end"> + + <include android:id="@+id/size_compat_hint" + layout="@layout/compat_mode_hint"/> <ImageButton android:id="@+id/size_compat_restart_button" - android:layout_width="@dimen/size_compat_button_size" - android:layout_height="@dimen/size_compat_button_size" - android:layout_gravity="center" - android:src="@drawable/size_compat_restart_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/compat_button_margin" + android:layout_marginBottom="@dimen/compat_button_margin" + android:src="@drawable/size_compat_restart_button_ripple" + android:background="@android:color/transparent" android:contentDescription="@string/restart_button_description"/> -</com.android.wm.shell.sizecompatui.SizeCompatRestartButton> +</com.android.wm.shell.compatui.CompatUILayout> diff --git a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml index ed5d2e1b49f5..d732b01ce106 100644 --- a/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml +++ b/libs/WindowManager/Shell/res/layout/docked_stack_divider.xml @@ -22,7 +22,7 @@ <View style="@style/DockedDividerBackground" android:id="@+id/docked_divider_background" - android:background="@color/docked_divider_background"/> + android:background="@color/split_divider_background"/> <com.android.wm.shell.legacysplitscreen.MinimizedDockShadow style="@style/DockedDividerMinimizedShadow" diff --git a/libs/WindowManager/Shell/res/layout/pip_menu.xml b/libs/WindowManager/Shell/res/layout/pip_menu.xml index 9fe024748610..1dd17bad155b 100644 --- a/libs/WindowManager/Shell/res/layout/pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/pip_menu.xml @@ -65,25 +65,29 @@ <LinearLayout android:id="@+id/top_end_container" android:layout_gravity="top|end" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> + <ImageButton android:id="@+id/settings" android:layout_width="@dimen/pip_action_size" android:layout_height="@dimen/pip_action_size" android:contentDescription="@string/pip_phone_settings" + android:layout_gravity="top|start" android:gravity="center" android:src="@drawable/pip_ic_settings" android:background="?android:selectableItemBackgroundBorderless" /> <ImageButton - android:id="@+id/dismiss" - android:layout_width="@dimen/pip_action_size" - android:layout_height="@dimen/pip_action_size" - android:contentDescription="@string/pip_phone_close" + android:id="@+id/enter_split" + android:layout_width="@dimen/pip_split_icon_size" + android:layout_height="@dimen/pip_split_icon_size" + android:layout_gravity="top|start" + android:layout_margin="@dimen/pip_split_icon_margin" android:gravity="center" - android:src="@drawable/pip_ic_close_white" + android:contentDescription="@string/pip_phone_enter_split" + android:src="@drawable/pip_split" android:background="?android:selectableItemBackgroundBorderless" /> </LinearLayout> @@ -97,4 +101,14 @@ android:padding="@dimen/pip_resize_handle_padding" android:src="@drawable/pip_resize_handle" android:background="?android:selectableItemBackgroundBorderless" /> + + <ImageButton + android:id="@+id/dismiss" + android:layout_width="@dimen/pip_action_size" + android:layout_height="@dimen/pip_action_size" + android:contentDescription="@string/pip_phone_close" + android:layout_gravity="top|end" + android:gravity="center" + android:src="@drawable/pip_ic_close_white" + android:background="?android:selectableItemBackgroundBorderless" /> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml b/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml deleted file mode 100644 index 0dea87c6b7fc..000000000000 --- a/libs/WindowManager/Shell/res/layout/size_compat_mode_hint.xml +++ /dev/null @@ -1,65 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2019 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. ---> -<com.android.wm.shell.sizecompatui.SizeCompatHintPopup - xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:gravity="center" - android:clipToPadding="false" - android:padding="@dimen/bubble_elevation"> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@android:color/background_light" - android:elevation="@dimen/bubble_elevation" - android:orientation="vertical"> - - <TextView - android:layout_width="180dp" - android:layout_height="wrap_content" - android:paddingLeft="10dp" - android:paddingRight="10dp" - android:paddingTop="10dp" - android:text="@string/restart_button_description" - android:textAlignment="viewStart" - android:textColor="@android:color/primary_text_light" - android:textSize="16sp"/> - - <Button - android:id="@+id/got_it" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:includeFontPadding="false" - android:layout_gravity="end" - android:minHeight="36dp" - android:background="?android:attr/selectableItemBackground" - android:text="@string/got_it" - android:textAllCaps="true" - android:textColor="#3c78d8" - android:textSize="16sp" - android:textStyle="bold"/> - - </LinearLayout> - - </FrameLayout> - -</com.android.wm.shell.sizecompatui.SizeCompatHintPopup> diff --git a/libs/WindowManager/Shell/res/layout/split_decor.xml b/libs/WindowManager/Shell/res/layout/split_decor.xml new file mode 100644 index 000000000000..9ffa5e8aa179 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/split_decor.xml @@ -0,0 +1,30 @@ +<!-- + ~ Copyright (C) 2021 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. + --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent"> + + <ImageView android:id="@+id/split_resizing_icon" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_gravity="center" + android:padding="0dp" + android:visibility="gone" + android:background="@null"/> + +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/layout/split_divider.xml b/libs/WindowManager/Shell/res/layout/split_divider.xml index 7f583f3e6bac..e3be700469a7 100644 --- a/libs/WindowManager/Shell/res/layout/split_divider.xml +++ b/libs/WindowManager/Shell/res/layout/split_divider.xml @@ -19,15 +19,25 @@ android:layout_height="match_parent" android:layout_width="match_parent"> - <View - style="@style/DockedDividerBackground" - android:id="@+id/docked_divider_background" - android:background="@color/docked_divider_background"/> + <FrameLayout + android:id="@+id/divider_bar" + android:layout_width="match_parent" + android:layout_height="match_parent"> - <com.android.wm.shell.common.split.DividerHandleView - style="@style/DockedDividerHandle" - android:id="@+id/docked_divider_handle" - android:contentDescription="@string/accessibility_divider" - android:background="@null"/> + <View + style="@style/DockedDividerBackground" + android:id="@+id/docked_divider_background"/> + + <com.android.wm.shell.common.split.DividerHandleView + style="@style/DockedDividerHandle" + android:id="@+id/docked_divider_handle" + android:contentDescription="@string/accessibility_divider" + android:background="@null"/> + + <com.android.wm.shell.common.split.DividerRoundedCorner + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + </FrameLayout> </com.android.wm.shell.common.split.DividerView> diff --git a/libs/WindowManager/Shell/res/layout/split_outline.xml b/libs/WindowManager/Shell/res/layout/split_outline.xml new file mode 100644 index 000000000000..6cb9ebb72790 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/split_outline.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.wm.shell.splitscreen.OutlineView + android:id="@+id/split_outline" + android:layout_height="match_parent" + android:layout_width="match_parent" /> + +</FrameLayout> diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index 69aa31ee861a..107da8149e5b 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Maak toe"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Vou uit"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Instellings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Gaan by verdeelde skerm in"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Kieslys"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in beeld-in-beeld"</string> <string name="pip_notification_message" msgid="8854051911700302620">"As jy nie wil hê dat <xliff:g id="NAME">%s</xliff:g> hierdie kenmerk moet gebruik nie, tik om instellings oop te maak en skakel dit af."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Bestuur"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Borrel is toegemaak."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tik om hierdie program te herbegin en maak volskerm oop."</string> - <string name="got_it" msgid="4428750913636945527">"Het dit"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index c754e3ca4dd2..d724372c3da3 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ዝጋ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ዘርጋ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ቅንብሮች"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"የተከፈለ ማያ ገጽን አስገባ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ምናሌ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ያቀናብሩ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"አረፋ ተሰናብቷል።"</string> <string name="restart_button_description" msgid="5887656107651190519">"ይህን መተግበሪያ ዳግም ለማስነሳት መታ ያድርጉ እና ወደ ሙሉ ማያ ገጽ ይሂዱ።"</string> - <string name="got_it" msgid="4428750913636945527">"ገባኝ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index ac72a3d7a367..7dd1f8151a72 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"إغلاق"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"توسيع"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"الإعدادات"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"الدخول في وضع تقسيم الشاشة"</string> <string name="pip_menu_title" msgid="5393619322111827096">"القائمة"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"إدارة"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"تم إغلاق الفقاعة."</string> <string name="restart_button_description" msgid="5887656107651190519">"انقر لإعادة تشغيل هذا التطبيق والانتقال إلى وضع ملء الشاشة."</string> - <string name="got_it" msgid="4428750913636945527">"حسنًا"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 325838581659..190f7ca9b96c 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"বন্ধ কৰক"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"বিস্তাৰ কৰক"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ছেটিং"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"বিভাজিত স্ক্ৰীন ম’ডলৈ যাওক"</string> <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"পৰিচালনা কৰক"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল অগ্ৰাহ্য কৰা হৈছে"</string> <string name="restart_button_description" msgid="5887656107651190519">"এপ্টো ৰিষ্টাৰ্ট কৰিবলৈ আৰু পূৰ্ণ স্ক্ৰীন ব্যৱহাৰ কৰিবলৈ টিপক।"</string> - <string name="got_it" msgid="4428750913636945527">"বুজি পালোঁ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 6d3e0a92d2ce..e33a35f975ad 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Bağlayın"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Genişləndirin"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana daxil olun"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> şəkil içində şəkildədir"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> tətbiqinin bu funksiyadan istifadə etməyini istəmirsinizsə, ayarları açmaq və deaktiv etmək üçün klikləyin."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"İdarə edin"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Qabarcıqdan imtina edilib."</string> <string name="restart_button_description" msgid="5887656107651190519">"Bu tətbiqi sıfırlayaraq tam ekrana keçmək üçün toxunun."</string> - <string name="got_it" msgid="4428750913636945527">"Anladım"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index 358da25d92c0..f59e9320c645 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Proširi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Podešavanja"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Uđi na podeljeni ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je slika u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da biste otvorili podešavanja i isključili je."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljajte"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> <string name="restart_button_description" msgid="5887656107651190519">"Dodirnite da biste restartovali aplikaciju i prešli u režim celog ekrana."</string> - <string name="got_it" msgid="4428750913636945527">"Važi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 7a934ccb1e9a..3b478f2ab6cb 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Закрыць"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Разгарнуць"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Налады"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Падзяліць экран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Усплывальнае апавяшчэнне адхілена."</string> <string name="restart_button_description" msgid="5887656107651190519">"Націсніце, каб перазапусціць гэту праграму і перайсці ў поўнаэкранны рэжым."</string> - <string name="got_it" msgid="4428750913636945527">"Зразумела"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 02930b1db277..3a77a1c7be28 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Затваряне"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Разгъване"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Преминаване към разделен екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Управление"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отхвърлено."</string> <string name="restart_button_description" msgid="5887656107651190519">"Докоснете, за да рестартирате това приложение в режим на цял екран."</string> - <string name="got_it" msgid="4428750913636945527">"Разбрах"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index b35e17919cc2..8bfd775704dd 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"বন্ধ করুন"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"বড় করুন"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"সেটিংস"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"\'স্প্লিট স্ক্রিন\' মোড চালু করুন"</string> <string name="pip_menu_title" msgid="5393619322111827096">"মেনু"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ম্যানেজ করুন"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল বাতিল করা হয়েছে।"</string> <string name="restart_button_description" msgid="5887656107651190519">"এই অ্যাপ রিস্টার্ট করতে ট্যাপ করুন ও \'ফুল-স্ক্রিন\' মোড ব্যবহার করুন।"</string> - <string name="got_it" msgid="4428750913636945527">"বুঝেছি"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 14d90a488352..d23cc61b52f1 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Proširi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvori podijeljeni ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je u načinu priakza Slika u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da <xliff:g id="NAME">%s</xliff:g> koristi ovu funkciju, dodirnite da otvorite postavke i isključite je."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljaj"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string> <string name="restart_button_description" msgid="5887656107651190519">"Dodirnite da ponovo pokrenete ovu aplikaciju i aktivirate prikaz preko cijelog ekrana."</string> - <string name="got_it" msgid="4428750913636945527">"Razumijem"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml index 8e301b0a8f4d..9a655bb41066 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings_tv.xml @@ -19,6 +19,6 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <string name="notification_channel_tv_pip" msgid="2576686079160402435">"Slika u slici"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(Program bez naslova)"</string> - <string name="pip_close" msgid="9135220303720555525">"Zatvori PIP"</string> + <string name="pip_close" msgid="9135220303720555525">"Zatvori sliku u slici"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"Cijeli ekran"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 9cffbdd7159e..6434e315503d 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Tanca"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Desplega"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configuració"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Entra al mode de pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> està en pantalla en pantalla"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no vols que <xliff:g id="NAME">%s</xliff:g> utilitzi aquesta funció, toca per obrir la configuració i desactiva-la."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestiona"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"La bombolla s\'ha ignorat."</string> <string name="restart_button_description" msgid="5887656107651190519">"Toca per reiniciar aquesta aplicació i passar a pantalla completa."</string> - <string name="got_it" msgid="4428750913636945527">"Entesos"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 9b5206a1da5c..3530a7c8b835 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zavřít"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Rozbalit"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavení"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivovat rozdělenou obrazovku"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Nabídka"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Aplikace <xliff:g id="NAME">%s</xliff:g> je v režimu obraz v obraze"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Pokud nechcete, aby aplikace <xliff:g id="NAME">%s</xliff:g> tuto funkci používala, klepnutím otevřete nastavení a funkci vypněte."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string> <string name="restart_button_description" msgid="5887656107651190519">"Klepnutím aplikaci restartujete a přejdete na režim celé obrazovky"</string> - <string name="got_it" msgid="4428750913636945527">"Rozumím"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index a06abf10cb23..89b66e5309e9 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Luk"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Udvid"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Indstillinger"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Åbn opdelt skærm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> vises som integreret billede"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke ønsker, at <xliff:g id="NAME">%s</xliff:g> skal benytte denne funktion, kan du åbne indstillingerne og deaktivere den."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen blev lukket."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tryk for at genstarte denne app, og gå til fuld skærm."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index c5e79f87ca50..b49b4462e476 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Schließen"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Maximieren"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Einstellungen"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"„Bildschirm teilen“ aktivieren"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ist in Bild im Bild"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Wenn du nicht möchtest, dass <xliff:g id="NAME">%s</xliff:g> diese Funktion verwendet, tippe, um die Einstellungen zu öffnen und die Funktion zu deaktivieren."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Verwalten"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble verworfen."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tippe, um die App im Vollbildmodus neu zu starten."</string> - <string name="got_it" msgid="4428750913636945527">"Ok"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index fc397c5569a8..ed1d9133eb92 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Κλείσιμο"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Ανάπτυξη"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ρυθμίσεις"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Μετάβαση σε διαχωρισμό οθόνης"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Μενού"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Η λειτουργία picture-in-picture είναι ενεργή σε <xliff:g id="NAME">%s</xliff:g>."</string> <string name="pip_notification_message" msgid="8854051911700302620">"Εάν δεν θέλετε να χρησιμοποιείται αυτή η λειτουργία από την εφαρμογή <xliff:g id="NAME">%s</xliff:g>, πατήστε για να ανοίξετε τις ρυθμίσεις και απενεργοποιήστε την."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Διαχείριση"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Το συννεφάκι παραβλέφθηκε."</string> <string name="restart_button_description" msgid="5887656107651190519">"Πατήστε για επανεκκίνηση αυτής της εφαρμογής και ενεργοποίηση πλήρους οθόνης."</string> - <string name="got_it" msgid="4428750913636945527">"Το κατάλαβα"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index a4f287f1bdb4..067e998ff396 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index a4f287f1bdb4..067e998ff396 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index a4f287f1bdb4..067e998ff396 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index a4f287f1bdb4..067e998ff396 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml index 87210d5ecbec..95c0d0175413 100644 --- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Close"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expand"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Settings"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Enter split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"If you don\'t want <xliff:g id="NAME">%s</xliff:g> to use this feature, tap to open settings and turn it off."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tap to restart this app and go full screen."</string> - <string name="got_it" msgid="4428750913636945527">"Got it"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index ebe41e88f08c..6e5347d1102c 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Cerrar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en modo de Pantalla en pantalla"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> use esta función, presiona para abrir la configuración y desactivarla."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Se descartó el cuadro."</string> <string name="restart_button_description" msgid="5887656107651190519">"Presiona para reiniciar esta app y acceder al modo de pantalla completa."</string> - <string name="got_it" msgid="4428750913636945527">"Entendido"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 5949099bd6a3..4820a0f1d75b 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Cerrar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Mostrar"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ajustes"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Introducir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está en imagen en imagen"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si no quieres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca la notificación para abrir los ajustes y desactivarla."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbuja cerrada."</string> <string name="restart_button_description" msgid="5887656107651190519">"Toca para reiniciar esta aplicación e ir a la pantalla completa."</string> - <string name="got_it" msgid="4428750913636945527">"Entendido"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 83309810311c..4c946948fd26 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Sule"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Laiendamine"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Seaded"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Ava jagatud ekraanikuva"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menüü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on režiimis Pilt pildis"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Kui te ei soovi, et rakendus <xliff:g id="NAME">%s</xliff:g> seda funktsiooni kasutaks, puudutage seadete avamiseks ja lülitage see välja."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Halda"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Mullist loobuti."</string> <string name="restart_button_description" msgid="5887656107651190519">"Puudutage rakenduse taaskäivitamiseks ja täisekraanrežiimi aktiveerimiseks."</string> - <string name="got_it" msgid="4428750913636945527">"Selge"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 664976983fdc..afc4292b7548 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Itxi"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Zabaldu"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ezarpenak"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Sartu pantaila zatituan"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menua"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Pantaila txiki gainjarrian dago <xliff:g id="NAME">%s</xliff:g>"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ez baduzu nahi <xliff:g id="NAME">%s</xliff:g> zerbitzuak eginbide hori erabiltzea, sakatu hau ezarpenak ireki eta aukera desaktibatzeko."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Kudeatu"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Baztertu da globoa."</string> <string name="restart_button_description" msgid="5887656107651190519">"Saka ezazu aplikazioa berrabiarazteko, eta ezarri pantaila osoko modua."</string> - <string name="got_it" msgid="4428750913636945527">"Ados"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index f646039df1f8..e8a0682b3418 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"بستن"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"بزرگ کردن"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"تنظیمات"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ورود به حالت «صفحهٔ دونیمه»"</string> <string name="pip_menu_title" msgid="5393619322111827096">"منو"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"مدیریت"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"حبابک رد شد."</string> <string name="restart_button_description" msgid="5887656107651190519">"برای بازراهاندازی این برنامه و تغییر به حالت تمامصفحه، ضربه بزنید."</string> - <string name="got_it" msgid="4428750913636945527">"متوجهام"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 5f871639a202..f6711055e4d9 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Sulje"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Laajenna"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Asetukset"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Avaa jaettu näyttö"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Valikko"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> on kuva kuvassa ‑tilassa"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jos et halua, että <xliff:g id="NAME">%s</xliff:g> voi käyttää tätä ominaisuutta, avaa asetukset napauttamalla ja poista se käytöstä."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Ylläpidä"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Kupla ohitettu."</string> <string name="restart_button_description" msgid="5887656107651190519">"Napauta, niin sovellus käynnistyy uudelleen ja siirtyy koko näytön tilaan."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index 68df5917eab4..0d0b71868170 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fermer"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Développer"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Entrer dans l\'écran partagé"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode d\'incrustation d\'image"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, touchez l\'écran pour ouvrir les paramètres, puis désactivez-la."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle ignorée."</string> <string name="restart_button_description" msgid="5887656107651190519">"Touchez pour redémarrer cette application et passer en plein écran."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index eecc9cbbba43..5652d7ee5398 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fermer"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Développer"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Paramètres"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accéder à l\'écran partagé"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> est en mode Picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Si vous ne voulez pas que l\'application <xliff:g id="NAME">%s</xliff:g> utilise cette fonctionnalité, appuyez ici pour ouvrir les paramètres et la désactiver."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle fermée."</string> <string name="restart_button_description" msgid="5887656107651190519">"Appuyez pour redémarrer cette application et activer le mode plein écran."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 3583cafdfb9f..81bd9167d0e6 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Pechar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Despregar"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configuración"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Inserir pantalla dividida"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menú"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está na pantalla superposta"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se non queres que <xliff:g id="NAME">%s</xliff:g> utilice esta función, toca a configuración para abrir as opcións e desactivar a función."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Xestionar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ignorouse a burbulla."</string> <string name="restart_button_description" msgid="5887656107651190519">"Toca o botón para reiniciar esta aplicación e abrila en pantalla completa."</string> - <string name="got_it" msgid="4428750913636945527">"Entendido"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index ad5a68e407a3..3d408cf2f698 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"બંધ કરો"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"વિસ્તૃત કરો"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"સેટિંગ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"વિભાજિત સ્ક્રીન મોડમાં દાખલ થાઓ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"મેનૂ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"મેનેજ કરો"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"બબલ છોડી દેવાયો."</string> <string name="restart_button_description" msgid="5887656107651190519">"આ ઍપ ફરીથી ચાલુ કરવા માટે ટૅપ કરીને પૂર્ણ સ્ક્રીન કરો."</string> - <string name="got_it" msgid="4428750913636945527">"સમજાઈ ગયું"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 55a30f2358fb..8c93e0a68da3 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"बंद करें"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तार करें"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन मोड में जाएं"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेन्यू"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"मैनेज करें"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल खारिज किया गया."</string> <string name="restart_button_description" msgid="5887656107651190519">"इस ऐप्लिकेशन को रीस्टार्ट करने और फ़ुल स्क्रीन पर देखने के लिए टैप करें."</string> - <string name="got_it" msgid="4428750913636945527">"ठीक है"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index f6acb5cb2d33..1f8f982fca69 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zatvori"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Proširivanje"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Postavke"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Otvorite podijeljeni zaslon"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Izbornik"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> jest na slici u slici"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ako ne želite da aplikacija <xliff:g id="NAME">%s</xliff:g> upotrebljava tu značajku, dodirnite da biste otvorili postavke i isključili je."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić odbačen."</string> <string name="restart_button_description" msgid="5887656107651190519">"Dodirnite da biste ponovo pokrenuli tu aplikaciju i prikazali je na cijelom zaslonu."</string> - <string name="got_it" msgid="4428750913636945527">"Shvaćam"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 0c1c8a40c8bf..ebd02e59a101 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Bezárás"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Kibontás"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Beállítások"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Váltás osztott képernyőre"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"A(z) <xliff:g id="NAME">%s</xliff:g> kép a képben funkciót használ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ha nem szeretné, hogy a(z) <xliff:g id="NAME">%s</xliff:g> használja ezt a funkciót, koppintson a beállítások megnyitásához, és kapcsolja ki."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Kezelés"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Buborék elvetve."</string> <string name="restart_button_description" msgid="5887656107651190519">"Koppintson az alkalmazás újraindításához és a teljes képernyős mód elindításához."</string> - <string name="got_it" msgid="4428750913636945527">"Rendben"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 36204c1a6599..29b20521829e 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Փակել"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Ընդարձակել"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Կարգավորումներ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Մտնել տրոհված էկրանի ռեժիմ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Ընտրացանկ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Կառավարել"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ամպիկը փակվեց։"</string> <string name="restart_button_description" msgid="5887656107651190519">"Հպեք՝ հավելվածը վերագործարկելու և լիաէկրան ռեժիմին անցնելու համար։"</string> - <string name="got_it" msgid="4428750913636945527">"Եղավ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index de962c43b137..6432aeff4630 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Tutup"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Luaskan"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Setelan"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk ke mode layar terpisah"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> adalah picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jika Anda tidak ingin <xliff:g id="NAME">%s</xliff:g> menggunakan fitur ini, ketuk untuk membuka setelan dan menonaktifkannya."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Kelola"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon ditutup."</string> <string name="restart_button_description" msgid="5887656107651190519">"Ketuk untuk memulai ulang aplikasi ini dan membuka layar penuh."</string> - <string name="got_it" msgid="4428750913636945527">"Oke"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index c205d22e7ae8..126d1f13ba03 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Loka"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Stækka"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Stillingar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Opna skjáskiptingu"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Valmynd"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er með mynd í mynd"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ef þú vilt ekki að <xliff:g id="NAME">%s</xliff:g> noti þennan eiginleika skaltu ýta til að opna stillingarnar og slökkva á því."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Stjórna"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Blöðru lokað."</string> <string name="restart_button_description" msgid="5887656107651190519">"Ýttu til að endurræsa forritið og sýna það á öllum skjánum."</string> - <string name="got_it" msgid="4428750913636945527">"Ég skil"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index c788a03a5b29..ec221b188563 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Chiudi"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Espandi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Impostazioni"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accedi a schermo diviso"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> è in Picture in picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se non desideri che l\'app <xliff:g id="NAME">%s</xliff:g> utilizzi questa funzione, tocca per aprire le impostazioni e disattivarla."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestisci"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Fumetto ignorato."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tocca per riavviare l\'app e passare alla modalità a schermo intero."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index b0c03edf168d..b87d10c22965 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"סגירה"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"הרחבה"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"הגדרות"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"כניסה למסך המפוצל"</string> <string name="pip_menu_title" msgid="5393619322111827096">"תפריט"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ניהול"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"הבועה נסגרה."</string> <string name="restart_button_description" msgid="5887656107651190519">"צריך להקיש כדי להפעיל מחדש את האפליקציה הזו ולעבור למסך מלא."</string> - <string name="got_it" msgid="4428750913636945527">"הבנתי"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 36700bd81717..51ffca6c0d2a 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"閉じる"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"分割画面に切り替え"</string> <string name="pip_menu_title" msgid="5393619322111827096">"メニュー"</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>でこの機能を使用しない場合は、タップして設定を開いて OFF にしてください。"</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ふきだしが非表示になっています。"</string> <string name="restart_button_description" msgid="5887656107651190519">"タップしてこのアプリを再起動すると、全画面表示になります。"</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index af1377a2f1e0..fc91d72179d3 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"დახურვა"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"გაშლა"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"პარამეტრები"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"გაყოფილ ეკრანში შესვლა"</string> <string name="pip_menu_title" msgid="5393619322111827096">"მენიუ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"მართვა"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ბუშტი დაიხურა."</string> <string name="restart_button_description" msgid="5887656107651190519">"შეეხეთ ამ აპის გადასატვირთად და გადადით სრულ ეკრანზე."</string> - <string name="got_it" msgid="4428750913636945527">"გასაგებია"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 6deb0b892316..05a905dac69f 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Жабу"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Жаю"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Параметрлер"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Бөлінген экранға кіру"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Mәзір"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқыма хабар жабылды."</string> <string name="restart_button_description" msgid="5887656107651190519">"Бұл қолданбаны қайта қосып, толық экранға өту үшін түртіңіз."</string> - <string name="got_it" msgid="4428750913636945527">"Түсінікті"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index c59d0fc4f60d..6a1cb2b2ca5d 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"បិទ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ពង្រីក"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ការកំណត់"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ចូលមុខងារបំបែកអេក្រង់"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ម៉ឺនុយ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"គ្រប់គ្រង"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"បានច្រានចោលសារលេចឡើង។"</string> <string name="restart_button_description" msgid="5887656107651190519">"ចុចដើម្បីចាប់ផ្ដើមកម្មវិធីនេះឡើងវិញ រួចចូលប្រើពេញអេក្រង់។"</string> - <string name="got_it" msgid="4428750913636945527">"យល់ហើយ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 5e655b4c7bee..aecb54b96839 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ಮುಚ್ಚಿ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ವಿಸ್ತೃತಗೊಳಿಸು"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ಸ್ಪ್ಲಿಟ್-ಸ್ಕ್ರೀನ್ಗೆ ಪ್ರವೇಶಿಸಿ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ಮೆನು"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ನಿರ್ವಹಿಸಿ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ಬಬಲ್ ವಜಾಗೊಳಿಸಲಾಗಿದೆ."</string> <string name="restart_button_description" msgid="5887656107651190519">"ಈ ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಲು ಮತ್ತು ಪೂರ್ಣ ಸ್ಕ್ರೀನ್ನಲ್ಲಿ ನೋಡಲು ಟ್ಯಾಪ್ ಮಾಡಿ."</string> - <string name="got_it" msgid="4428750913636945527">"ಅರ್ಥವಾಯಿತು"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index af34ef48cb31..5af9ca2a0221 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"닫기"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"펼치기"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"설정"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"화면 분할 모드로 전환"</string> <string name="pip_menu_title" msgid="5393619322111827096">"메뉴"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>에서 PIP 사용 중"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g>에서 이 기능이 사용되는 것을 원하지 않는 경우 탭하여 설정을 열고 기능을 사용 중지하세요."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"관리"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"대화창을 닫았습니다."</string> <string name="restart_button_description" msgid="5887656107651190519">"탭하여 이 앱을 다시 시작하고 전체 화면으로 이동합니다."</string> - <string name="got_it" msgid="4428750913636945527">"확인"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 8056d15de3f4..76f192ed0414 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Жабуу"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Жайып көрсөтүү"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Жөндөөлөр"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Экранды бөлүү режимине өтүү"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Башкаруу"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Калкып чыкма билдирме жабылды."</string> <string name="restart_button_description" msgid="5887656107651190519">"Бул колдонмону өчүрүп күйгүзүп, толук экранга өтүү үчүн таптап коюңуз."</string> - <string name="got_it" msgid="4428750913636945527">"Түшүндүм"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-land/dimens.xml b/libs/WindowManager/Shell/res/values-land/dimens.xml index aafba58cef59..a95323fd4801 100644 --- a/libs/WindowManager/Shell/res/values-land/dimens.xml +++ b/libs/WindowManager/Shell/res/values-land/dimens.xml @@ -16,8 +16,12 @@ */ --> <resources> + <!-- Divider handle size for legacy split screen --> <dimen name="docked_divider_handle_width">2dp</dimen> <dimen name="docked_divider_handle_height">16dp</dimen> + <!-- Divider handle size for split screen --> + <dimen name="split_divider_handle_width">3dp</dimen> + <dimen name="split_divider_handle_height">72dp</dimen> <!-- Padding between status bar and bubbles when displayed in expanded state, smaller value in landscape since we have limited vertical space--> diff --git a/libs/WindowManager/Shell/res/values-land/styles.xml b/libs/WindowManager/Shell/res/values-land/styles.xml index 863bb69d4034..0ed9368aa067 100644 --- a/libs/WindowManager/Shell/res/values-land/styles.xml +++ b/libs/WindowManager/Shell/res/values-land/styles.xml @@ -19,10 +19,11 @@ <item name="android:layout_width">10dp</item> <item name="android:layout_height">match_parent</item> <item name="android:layout_gravity">center_horizontal</item> + <item name="android:background">@color/split_divider_background</item> </style> <style name="DockedDividerHandle"> - <item name="android:layout_gravity">center_vertical</item> + <item name="android:layout_gravity">center</item> <item name="android:layout_width">48dp</item> <item name="android:layout_height">96dp</item> </style> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index a578b0a3f4cd..4ec6313f8c8c 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ປິດ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ຂະຫຍາຍ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ການຕັ້ງຄ່າ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ເຂົ້າການແບ່ງໜ້າຈໍ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ເມນູ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ຈັດການ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ປິດ Bubble ໄສ້ແລ້ວ."</string> <string name="restart_button_description" msgid="5887656107651190519">"ແຕະເພື່ອຣີສະຕາດແອັບນີ້ ແລະ ໃຊ້ແບບເຕັມຈໍ."</string> - <string name="got_it" msgid="4428750913636945527">"ເຂົ້າໃຈແລ້ວ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index e037839a54db..8630e915aa09 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Uždaryti"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Išskleisti"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nustatymai"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Įjungti išskaidyto ekrano režimą"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> rodom. vaizdo vaizde"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jei nenorite, kad „<xliff:g id="NAME">%s</xliff:g>“ naudotų šią funkciją, palietę atidarykite nustatymus ir išjunkite ją."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Tvarkyti"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Debesėlio atsisakyta."</string> <string name="restart_button_description" msgid="5887656107651190519">"Palieskite, kad paleistumėte iš naujo šią programą ir įjungtumėte viso ekrano režimą."</string> - <string name="got_it" msgid="4428750913636945527">"Supratau"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 05472e454f77..b095b88bfa0c 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Aizvērt"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Izvērst"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Iestatījumi"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Piekļūt ekrāna sadalīšanas režīmam"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Izvēlne"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> ir attēlā attēlā"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ja nevēlaties lietotnē <xliff:g id="NAME">%s</xliff:g> izmantot šo funkciju, pieskarieties, lai atvērtu iestatījumus un izslēgtu funkciju."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Pārvaldīt"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbulis ir noraidīts."</string> <string name="restart_button_description" msgid="5887656107651190519">"Pieskarieties, lai restartētu šo lietotni un pārietu pilnekrāna režīmā."</string> - <string name="got_it" msgid="4428750913636945527">"Labi"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 9cb2c6906c70..184fe9d52283 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Затвори"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Проширете"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Поставки"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Влези во поделен екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Управувајте"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отфрлено."</string> <string name="restart_button_description" msgid="5887656107651190519">"Допрете за да ја рестартирате апликацијава и да ја отворите на цел екран."</string> - <string name="got_it" msgid="4428750913636945527">"Сфатив"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index f0bf513e264c..f1bfe9aa055e 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"അവസാനിപ്പിക്കുക"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"വികസിപ്പിക്കുക"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ക്രമീകരണം"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"സ്ക്രീൻ വിഭജന മോഡിൽ പ്രവേശിക്കുക"</string> <string name="pip_menu_title" msgid="5393619322111827096">"മെനു"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"മാനേജ് ചെയ്യുക"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബിൾ ഡിസ്മിസ് ചെയ്തു."</string> <string name="restart_button_description" msgid="5887656107651190519">"ഈ ആപ്പ് റീസ്റ്റാർട്ട് ചെയ്ത് പൂർണ്ണ സ്ക്രീനിലേക്ക് മാറാൻ ടാപ്പ് ചെയ്യുക."</string> - <string name="got_it" msgid="4428750913636945527">"മനസ്സിലായി"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 68822cb4f51b..8b8cb95d1e9b 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Хаах"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Дэлгэх"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Тохиргоо"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Хуваасан дэлгэцийг оруулна уу"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Цэс"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Удирдах"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Бөмбөлгийг үл хэрэгссэн."</string> <string name="restart_button_description" msgid="5887656107651190519">"Энэ аппыг дахин эхлүүлж, бүтэн дэлгэцэд орохын тулд товшино уу."</string> - <string name="got_it" msgid="4428750913636945527">"Ойлголоо"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index a4b7be4d52c7..c11af7bf9f2c 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"बंद करा"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तृत करा"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिंग्ज"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रीन एंटर करा"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेनू"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापित करा"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल डिसमिस केला."</string> <string name="restart_button_description" msgid="5887656107651190519">"हे अॅप रीस्टार्ट करण्यासाठी आणि फुल स्क्रीन करण्यासाठी टॅप करा."</string> - <string name="got_it" msgid="4428750913636945527">"समजले"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 2f33bfa41d83..5493ce5a4fab 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Tutup"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Kembangkan"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Tetapan"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Masuk skrin pisah"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> terdapat dalam gambar dalam gambar"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jika anda tidak mahu <xliff:g id="NAME">%s</xliff:g> menggunakan ciri ini, ketik untuk membuka tetapan dan matikan ciri."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Urus"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Gelembung diketepikan."</string> <string name="restart_button_description" msgid="5887656107651190519">"Ketik untuk memulakan semula apl ini dan menggunakan skrin penuh."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 018ffc02556d..e1d17f88fb9b 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -20,8 +20,9 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ပိတ်ရန်"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ချဲ့ရန်"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ဆက်တင်များ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"မျက်နှာပြင် ခွဲ၍ပြသခြင်းသို့ ဝင်ရန်"</string> <string name="pip_menu_title" msgid="5393619322111827096">"မီနူး"</string> - <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> သည် တစ်ခုပေါ် တစ်ခုထပ်၍ ဖွင့်ထားသည်"</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_play" msgid="3496151081459417097">"ဖွင့်ရန်"</string> <string name="pip_pause" msgid="690688849510295232">"ခေတ္တရပ်ရန်"</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"စီမံရန်"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ပူဖောင်းကွက် ဖယ်လိုက်သည်။"</string> <string name="restart_button_description" msgid="5887656107651190519">"ဤအက်ပ်ကို ပြန်စပြီး ဖန်သားပြင်အပြည့်လုပ်ရန် တို့ပါ။"</string> - <string name="got_it" msgid="4428750913636945527">"ရပြီ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-my/strings_tv.xml b/libs/WindowManager/Shell/res/values-my/strings_tv.xml index 9569dc4cbeea..c18d53932163 100644 --- a/libs/WindowManager/Shell/res/values-my/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values-my/strings_tv.xml @@ -17,7 +17,7 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="notification_channel_tv_pip" msgid="2576686079160402435">"တစ်ခုပေါ်တစ်ခုထပ်၍ ဖွင့်ခြင်း"</string> + <string name="notification_channel_tv_pip" msgid="2576686079160402435">"နှစ်ခုထပ်၍ကြည့်ခြင်း"</string> <string name="pip_notification_unknown_title" msgid="2729870284350772311">"(ခေါင်းစဉ်မဲ့ အစီအစဉ်)"</string> <string name="pip_close" msgid="9135220303720555525">"PIP ကိုပိတ်ပါ"</string> <string name="pip_fullscreen" msgid="7278047353591302554">"မျက်နှာပြင် အပြည့်"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index a23ad9068ea9..87bc7dafe902 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Lukk"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Vis"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Innstillinger"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aktivér delt skjerm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> er i bilde-i-bilde"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Hvis du ikke vil at <xliff:g id="NAME">%s</xliff:g> skal bruke denne funksjonen, kan du trykke for å åpne innstillingene og slå den av."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string> <string name="restart_button_description" msgid="5887656107651190519">"Trykk for å starte denne appen på nytt og vise den i fullskjerm."</string> - <string name="got_it" msgid="4428750913636945527">"Greit"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 5b9b87205428..12df158f0903 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"बन्द गर्नुहोस्"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"विस्तृत गर्नुहोस्"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"सेटिङहरू"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"स्प्लिट स्क्रिन मोड प्रयोग गर्नुहोस्"</string> <string name="pip_menu_title" msgid="5393619322111827096">"मेनु"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> Picture-in-picture मा छ"</string> <string name="pip_notification_message" msgid="8854051911700302620">"तपाईं <xliff:g id="NAME">%s</xliff:g> ले सुविधा प्रयोग नगरोस् भन्ने चाहनुहुन्छ भने ट्याप गरेर सेटिङहरू खोल्नुहोस् र यसलाई निष्क्रिय पार्नुहोस्।"</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापन गर्नुहोस्"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल हटाइयो।"</string> <string name="restart_button_description" msgid="5887656107651190519">"यो एप रिस्टार्ट गर्न ट्याप गर्नुहोस् र फुल स्क्रिन मोडमा जानुहोस्।"</string> - <string name="got_it" msgid="4428750913636945527">"बुझेँ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index 06aaad7d65ca..f83ad22dfea7 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Sluiten"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Uitvouwen"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Instellingen"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Gesplitst scherm openen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> is in scherm-in-scherm"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Als je niet wilt dat <xliff:g id="NAME">%s</xliff:g> deze functie gebruikt, tik je om de instellingen te openen en zet je de functie uit."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Beheren"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubbel gesloten."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tik om deze app opnieuw te starten en te openen op het volledige scherm."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index ac1e84a6e283..14f92c8f1d1c 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ବନ୍ଦ କରନ୍ତୁ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ବଢ଼ାନ୍ତୁ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ସେଟିଂସ୍"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ମୋଡ ବ୍ୟବହାର କରନ୍ତୁ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ମେନୁ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ପରିଚାଳନା କରନ୍ତୁ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ବବଲ୍ ଖାରଜ କରାଯାଇଛି।"</string> <string name="restart_button_description" msgid="5887656107651190519">"ଏହି ଆପକୁ ରିଷ୍ଟାର୍ଟ କରି ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନ୍ କରିବାକୁ ଟାପ୍ କରନ୍ତୁ।"</string> - <string name="got_it" msgid="4428750913636945527">"ବୁଝିଗଲି"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index bf5b733c22ea..e09f53adba43 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ਬੰਦ ਕਰੋ"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ਵਿਸਤਾਰ ਕਰੋ"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ਸੈਟਿੰਗਾਂ"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਵਿੱਚ ਦਾਖਲ ਹੋਵੋ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"ਮੀਨੂ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕੀਤਾ ਗਿਆ।"</string> <string name="restart_button_description" msgid="5887656107651190519">"ਇਸ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ ਅਤੇ ਪੂਰੀ ਸਕ੍ਰੀਨ ਮੋਡ \'ਤੇ ਜਾਓ।"</string> - <string name="got_it" msgid="4428750913636945527">"ਸਮਝ ਲਿਆ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index cd659ba86319..a2ef9975e487 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zamknij"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Rozwiń"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ustawienia"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Włącz podzielony ekran"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Aplikacja <xliff:g id="NAME">%s</xliff:g> działa w trybie obraz w obrazie"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Jeśli nie chcesz, by aplikacja <xliff:g id="NAME">%s</xliff:g> korzystała z tej funkcji, otwórz ustawienia i wyłącz ją."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Zarządzaj"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Zamknięto dymek"</string> <string name="restart_button_description" msgid="5887656107651190519">"Kliknij, by uruchomić tę aplikację ponownie i przejść w tryb pełnoekranowy."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index 3c8aaa4e0778..1300e530c0a6 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Abrir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> <string name="restart_button_description" msgid="5887656107651190519">"Toque para reiniciar o app e usar tela cheia."</string> - <string name="got_it" msgid="4428750913636945527">"Ok"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 1f5b0abd80d9..f3314f80cdfe 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Expandir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Definições"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Aceder ao ecrã dividido"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"A app <xliff:g id="NAME">%s</xliff:g> está no modo de ecrã no ecrã"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se não pretende que a app <xliff:g id="NAME">%s</xliff:g> utilize esta funcionalidade, toque para abrir as definições e desative-a."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerir"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão ignorado."</string> <string name="restart_button_description" msgid="5887656107651190519">"Toque para reiniciar esta app e ficar em ecrã inteiro."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index 3c8aaa4e0778..1300e530c0a6 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Fechar"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Abrir"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Configurações"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Dividir tela"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> está em picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Se você não quer que o app <xliff:g id="NAME">%s</xliff:g> use este recurso, toque para abrir as configurações e desativá-lo."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string> <string name="restart_button_description" msgid="5887656107651190519">"Toque para reiniciar o app e usar tela cheia."</string> - <string name="got_it" msgid="4428750913636945527">"Ok"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index d694be1cdd18..01f96c881b7e 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Închideți"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Extindeți"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Setări"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Accesați ecranul împărțit"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meniu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> este în modul picture-in-picture"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Dacă nu doriți ca <xliff:g id="NAME">%s</xliff:g> să utilizeze această funcție, atingeți pentru a deschide setările și dezactivați-o."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionați"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string> <string name="restart_button_description" msgid="5887656107651190519">"Atingeți ca să reporniți aplicația și să treceți în modul ecran complet."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index e9bfffbad399..6a0e9c12fe7f 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Закрыть"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Развернуть"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Настройки"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Включить разделение экрана"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Настроить"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Всплывающий чат закрыт."</string> <string name="restart_button_description" msgid="5887656107651190519">"Нажмите, чтобы перезапустить приложение и перейти в полноэкранный режим."</string> - <string name="got_it" msgid="4428750913636945527">"ОК"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index ba178f03efbb..d7ed24606f08 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"වසන්න"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"දිග හරින්න"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"සැකසීම්"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"බෙදුම් තිරයට ඇතුළු වන්න"</string> <string name="pip_menu_title" msgid="5393619322111827096">"මෙනුව"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"කළමනා කරන්න"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"බුබුල ඉවත දමා ඇත."</string> <string name="restart_button_description" msgid="5887656107651190519">"මෙම යෙදුම යළි ඇරඹීමට සහ පූර්ණ තිරයට යාමට තට්ටු කරන්න."</string> - <string name="got_it" msgid="4428750913636945527">"තේරුණා"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index e048ca15b91f..13fd58f801ea 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zavrieť"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Rozbaliť"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavenia"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Prejsť na rozdelenú obrazovku"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Ponuka"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v režime obraz v obraze"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ak nechcete, aby aplikácia <xliff:g id="NAME">%s</xliff:g> používala túto funkciu, klepnutím otvorte nastavenia a vypnite ju."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovať"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina bola zavretá."</string> <string name="restart_button_description" msgid="5887656107651190519">"Klepnutím reštartujete túto aplikáciu a prejdete do režimu celej obrazovky."</string> - <string name="got_it" msgid="4428750913636945527">"Dobre"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index ed05908c6e03..6a6806921c18 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Zapri"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Razširi"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Nastavitve"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Vklopi razdeljen zaslon"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meni"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> je v načinu slika v sliki"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Če ne želite, da aplikacija <xliff:g id="NAME">%s</xliff:g> uporablja to funkcijo, se dotaknite, da odprete nastavitve, in funkcijo izklopite."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblaček je bil opuščen."</string> <string name="restart_button_description" msgid="5887656107651190519">"Dotaknite se za vnovični zagon te aplikacije in preklop v celozaslonski način."</string> - <string name="got_it" msgid="4428750913636945527">"Razumem"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index 13e830cbd100..7382a480fb1e 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Mbyll"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Zgjero"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Cilësimet"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Hyr në ekranin e ndarë"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyja"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> është në figurë brenda figurës"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Nëse nuk dëshiron që <xliff:g id="NAME">%s</xliff:g> ta përdorë këtë funksion, trokit për të hapur cilësimet dhe për ta çaktivizuar."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Menaxho"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Flluska u hoq."</string> <string name="restart_button_description" msgid="5887656107651190519">"Trokit për ta rinisur këtë aplikacion dhe për të kaluar në ekranin e plotë."</string> - <string name="got_it" msgid="4428750913636945527">"E kuptova"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index be6857b1f1a8..c0c1e3f2849e 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Затвори"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Прошири"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Подешавања"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Уђи на подељени екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Мени"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Управљајте"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Облачић је одбачен."</string> <string name="restart_button_description" msgid="5887656107651190519">"Додирните да бисте рестартовали апликацију и прешли у режим целог екрана."</string> - <string name="got_it" msgid="4428750913636945527">"Важи"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index e61e69b51c5f..34254d90a93d 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Stäng"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Utöka"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Inställningar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Starta delad skärm"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Meny"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> visas i bild-i-bild"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Om du inte vill att den här funktionen används i <xliff:g id="NAME">%s</xliff:g> öppnar du inställningarna genom att trycka. Sedan inaktiverar du funktionen."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Hantera"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubblan ignorerades."</string> <string name="restart_button_description" msgid="5887656107651190519">"Tryck för att starta om appen i helskärmsläge."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 476af11cd3ad..82a9f146c449 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Funga"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Panua"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Mipangilio"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Weka skrini iliyogawanywa"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> iko katika hali ya picha ndani ya picha nyingine"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Ikiwa hutaki <xliff:g id="NAME">%s</xliff:g> itumie kipengele hiki, gusa ili ufungue mipangilio na uizime."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Dhibiti"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Umeondoa kiputo."</string> <string name="restart_button_description" msgid="5887656107651190519">"Gusa ili uzime na uwashe programu hii, kisha nenda kwenye skrini nzima."</string> - <string name="got_it" msgid="4428750913636945527">"Nimeelewa"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index bc27389c6116..0ed778a273af 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"மூடு"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"விரி"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"அமைப்புகள்"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"திரைப் பிரிப்பு பயன்முறைக்குச் செல்"</string> <string name="pip_menu_title" msgid="5393619322111827096">"மெனு"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"நிர்வகி"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"குமிழ் நிராகரிக்கப்பட்டது."</string> <string name="restart_button_description" msgid="5887656107651190519">"தட்டுவதன் மூலம் இந்த ஆப்ஸை மீண்டும் தொடங்கலாம், முழுத்திரையில் பார்க்கலாம்."</string> - <string name="got_it" msgid="4428750913636945527">"சரி"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index c2b6ffbd1048..8fef67b69747 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"మూసివేయి"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"విస్తరింపజేయి"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"సెట్టింగ్లు"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"స్ప్లిట్ స్క్రీన్ను ఎంటర్ చేయండి"</string> <string name="pip_menu_title" msgid="5393619322111827096">"మెనూ"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"మేనేజ్ చేయండి"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"బబుల్ విస్మరించబడింది."</string> <string name="restart_button_description" msgid="5887656107651190519">"ఈ యాప్ను రీస్టార్ట్ చేయడానికి ట్యాప్ చేసి, ఆపై పూర్తి స్క్రీన్లోకి వెళ్లండి."</string> - <string name="got_it" msgid="4428750913636945527">"అర్థమైంది"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 9017b3f6b326..06b04f145772 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"ปิด"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"ขยาย"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"การตั้งค่า"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"เข้าสู่โหมดแบ่งหน้าจอ"</string> <string name="pip_menu_title" msgid="5393619322111827096">"เมนู"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"จัดการ"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ปิดบับเบิลแล้ว"</string> <string name="restart_button_description" msgid="5887656107651190519">"แตะเพื่อรีสตาร์ทแอปนี้และแสดงแบบเต็มหน้าจอ"</string> - <string name="got_it" msgid="4428750913636945527">"รับทราบ"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index c484cafb191a..62642c18937e 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Isara"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Palawakin"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Mga Setting"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Pumasok sa split screen"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"Nasa picture-in-picture ang <xliff:g id="NAME">%s</xliff:g>"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Kung ayaw mong magamit ni <xliff:g id="NAME">%s</xliff:g> ang feature na ito, i-tap upang buksan ang mga setting at i-off ito."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Pamahalaan"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Na-dismiss na ang bubble."</string> <string name="restart_button_description" msgid="5887656107651190519">"I-tap para i-restart ang app na ito at mag-full screen."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index ca856a1fe93b..971520a0f229 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Kapat"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Genişlet"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Ayarlar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Bölünmüş ekrana geç"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menü"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g>, pencere içinde pencere özelliğini kullanıyor"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> uygulamasının bu özelliği kullanmasını istemiyorsanız dokunarak ayarları açın ve söz konusu özelliği kapatın."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Yönet"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon kapatıldı."</string> <string name="restart_button_description" msgid="5887656107651190519">"Bu uygulamayı yeniden başlatmak ve tam ekrana geçmek için dokunun."</string> - <string name="got_it" msgid="4428750913636945527">"Anladım"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index 08e8d2942d57..30147353e3f6 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Закрити"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Розгорнути"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Налаштування"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Розділити екран"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Меню"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Налаштувати"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Спливаюче сповіщення закрито."</string> <string name="restart_button_description" msgid="5887656107651190519">"Натисніть, щоб перезапустити додаток і перейти в повноекранний режим."</string> - <string name="got_it" msgid="4428750913636945527">"Зрозуміло"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 06c09276e717..07319efdc52c 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"بند کریں"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"پھیلائیں"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"ترتیبات"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"اسپلٹ اسکرین تک رسائی"</string> <string name="pip_menu_title" msgid="5393619322111827096">"مینو"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"نظم کریں"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"بلبلہ برخاست کر دیا گیا۔"</string> <string name="restart_button_description" msgid="5887656107651190519">"یہ ایپ دوبارہ شروع کرنے کے لیے تھپتھپائیں اور پوری اسکرین پر جائیں۔"</string> - <string name="got_it" msgid="4428750913636945527">"سمجھ آ گئی"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index 6a873a3e185e..4c79d64fb8e1 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Yopish"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Yoyish"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Sozlamalar"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Ajratilgan ekranga kirish"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> tasvir ustida tasvir rejimida"</string> <string name="pip_notification_message" msgid="8854051911700302620">"<xliff:g id="NAME">%s</xliff:g> ilovasi uchun bu funksiyani sozlamalar orqali faolsizlantirish mumkin."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Boshqarish"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulutcha yopildi."</string> <string name="restart_button_description" msgid="5887656107651190519">"Bu ilovani qaytadan ishga tushirish va butun ekranda ochish uchun bosing."</string> - <string name="got_it" msgid="4428750913636945527">"OK"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 4d4eebcfd68b..b9f23cd4672d 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Đóng"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Mở rộng"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Cài đặt"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Truy cập chế độ chia đôi màn hình"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Menu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"<xliff:g id="NAME">%s</xliff:g> đang ở chế độ ảnh trong ảnh"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Nếu bạn không muốn <xliff:g id="NAME">%s</xliff:g> sử dụng tính năng này, hãy nhấn để mở cài đặt và tắt tính năng này."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Quản lý"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Đã đóng bong bóng."</string> <string name="restart_button_description" msgid="5887656107651190519">"Nhấn để khởi động lại ứng dụng này và xem ở chế độ toàn màn hình."</string> - <string name="got_it" msgid="4428750913636945527">"Tôi hiểu"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 3b8c8894c4e7..c0072582ec88 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"关闭"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展开"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"设置"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"进入分屏模式"</string> <string name="pip_menu_title" msgid="5393619322111827096">"菜单"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已关闭对话泡。"</string> <string name="restart_button_description" msgid="5887656107651190519">"点按即可重启此应用并进入全屏模式。"</string> - <string name="got_it" msgid="4428750913636945527">"知道了"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 9ba82b5ddf72..5e336770e83a 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"關閉"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割螢幕"</string> <string name="pip_menu_title" msgid="5393619322111827096">"選單"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"對話氣泡已關閉。"</string> <string name="restart_button_description" msgid="5887656107651190519">"輕按即可重新開啟此應用程式並放大至全螢幕。"</string> - <string name="got_it" msgid="4428750913636945527">"知道了"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index aa666531996d..2439a975daa8 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"關閉"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"展開"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"設定"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"進入分割畫面"</string> <string name="pip_menu_title" msgid="5393619322111827096">"選單"</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> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已關閉泡泡。"</string> <string name="restart_button_description" msgid="5887656107651190519">"輕觸即可重新啟動這個應用程式並進入全螢幕模式。"</string> - <string name="got_it" msgid="4428750913636945527">"我知道了"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index c8199c844d5b..20128f602abf 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -20,6 +20,7 @@ <string name="pip_phone_close" msgid="5783752637260411309">"Vala"</string> <string name="pip_phone_expand" msgid="2579292903468287504">"Nweba"</string> <string name="pip_phone_settings" msgid="5468987116750491918">"Izilungiselelo"</string> + <string name="pip_phone_enter_split" msgid="7042877263880641911">"Faka ukuhlukanisa isikrini"</string> <string name="pip_menu_title" msgid="5393619322111827096">"Imenyu"</string> <string name="pip_notification_title" msgid="1347104727641353453">"U-<xliff:g id="NAME">%s</xliff:g> ungaphakathi kwesithombe esiphakathi kwesithombe"</string> <string name="pip_notification_message" msgid="8854051911700302620">"Uma ungafuni i-<xliff:g id="NAME">%s</xliff:g> ukuthi isebenzise lesi sici, thepha ukuze uvule izilungiselelo uphinde uyivale."</string> @@ -72,5 +73,4 @@ <string name="manage_bubbles_text" msgid="7730624269650594419">"Phatha"</string> <string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ibhamuza licashisiwe."</string> <string name="restart_button_description" msgid="5887656107651190519">"Thepha ukuze uqale kabusha lolu hlelo lokusebenza uphinde uye kusikrini esigcwele."</string> - <string name="got_it" msgid="4428750913636945527">"Ngiyezwa"</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml index 350beafae961..cf596f7d15dc 100644 --- a/libs/WindowManager/Shell/res/values/colors.xml +++ b/libs/WindowManager/Shell/res/values/colors.xml @@ -17,7 +17,6 @@ */ --> <resources> - <color name="docked_divider_background">#ff000000</color> <color name="docked_divider_handle">#ffffff</color> <drawable name="forced_resizable_background">#59000000</drawable> <color name="minimize_dock_shadow_start">#60000000</color> @@ -31,6 +30,10 @@ <color name="bubbles_dark">@color/GM2_grey_800</color> <color name="bubbles_icon_tint">@color/GM2_grey_700</color> + <!-- Compat controls UI --> + <color name="compat_controls_background">@android:color/system_neutral1_800</color> + <color name="compat_controls_text">@android:color/system_neutral1_50</color> + <!-- GM2 colors --> <color name="GM2_grey_200">#E8EAED</color> <color name="GM2_grey_700">#5F6368</color> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index d0e4f7a02ffc..1b8032b7077b 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -15,6 +15,10 @@ limitations under the License. --> <resources> + <!-- Determines whether the shell features all run on another thread. This is to be overrided + by the resources of the app using the Shell library. --> + <bool name="config_enableShellMainThread">false</bool> + <!-- Animation duration for PIP when entering. --> <integer name="config_pipEnterAnimationDuration">425</integer> @@ -39,6 +43,9 @@ <!-- PiP minimum size, which is a % based off the shorter side of display width and height --> <fraction name="config_pipShortestEdgePercent">40%</fraction> + <!-- Show PiP enter split icon, which allows apps to directly enter splitscreen from PiP. --> + <bool name="config_pipEnableEnterSplitButton">false</bool> + <!-- Animation duration when using long press on recents to dock --> <integer name="long_press_dock_anim_duration">250</integer> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index f28ee820eb35..af78293eb3ea 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -67,6 +67,10 @@ <dimen name="pip_resize_handle_margin">4dp</dimen> <dimen name="pip_resize_handle_padding">0dp</dimen> + <!-- PIP Split icon size and margin. --> + <dimen name="pip_split_icon_size">24dp</dimen> + <dimen name="pip_split_icon_margin">12dp</dimen> + <!-- PIP stash offset size, which is the width of visible PIP region when stashed. --> <dimen name="pip_stash_offset">32dp</dimen> @@ -76,8 +80,15 @@ <!-- How high we lift the divider when touching --> <dimen name="docked_stack_divider_lift_elevation">4dp</dimen> + <!-- Divider handle size for legacy split screen --> <dimen name="docked_divider_handle_width">16dp</dimen> <dimen name="docked_divider_handle_height">2dp</dimen> + <!-- Divider handle size for split screen --> + <dimen name="split_divider_handle_width">72dp</dimen> + <dimen name="split_divider_handle_height">3dp</dimen> + + <dimen name="split_divider_bar_width">10dp</dimen> + <dimen name="split_divider_corner_size">42dp</dimen> <!-- One-Handed Mode --> <!-- Threshold for dragging distance to enable one-handed mode --> @@ -100,12 +111,16 @@ <dimen name="bubble_flyout_space_from_bubble">8dp</dimen> <!-- How much space to leave between the flyout text and the avatar displayed in the flyout. --> <dimen name="bubble_flyout_avatar_message_space">6dp</dimen> + <!-- If the screen percentage is smaller than this, we'll use this value instead. --> + <dimen name="bubbles_flyout_min_width_large_screen">200dp</dimen> <!-- Padding between status bar and bubbles when displayed in expanded state --> <dimen name="bubble_padding_top">16dp</dimen> <!-- Space between bubbles when expanded. --> <dimen name="bubble_spacing">3dp</dimen> <!-- Size of the bubble. --> <dimen name="bubble_size">60dp</dimen> + <!-- Width of bubble name view --> + <dimen name="bubble_name_width">90dp</dimen> <!-- Size of the badge shown on the bubble. --> <dimen name="bubble_badge_size">24dp</dimen> <!-- Extra padding added to the touchable rect for bubbles so they are easier to grab. --> @@ -122,7 +137,7 @@ should also be updated. --> <dimen name="bubble_expanded_default_height">180dp</dimen> <!-- On large screens the width of the expanded view is restricted to this size. --> - <dimen name="bubble_expanded_view_tablet_width">412dp</dimen> + <dimen name="bubble_expanded_view_phone_landscape_overflow_width">412dp</dimen> <!-- Inset to apply to the icon in the overflow button. --> <dimen name="bubble_overflow_icon_inset">30dp</dimen> <!-- Default (and minimum) height of bubble overflow --> @@ -149,9 +164,17 @@ <!-- Extra padding around the dismiss target for bubbles --> <dimen name="bubble_dismiss_slop">16dp</dimen> <!-- Height of button allowing users to adjust settings for bubbles. --> - <dimen name="bubble_manage_button_height">56dp</dimen> + <dimen name="bubble_manage_button_height">36dp</dimen> + <!-- Height of manage button including margins. --> + <dimen name="bubble_manage_button_total_height">68dp</dimen> + <!-- The margin around the outside of the manage button. --> + <dimen name="bubble_manage_button_margin">16dp</dimen> <!-- Height of an item in the bubble manage menu. --> <dimen name="bubble_menu_item_height">60dp</dimen> + <!-- Padding applied to the bubble manage menu. --> + <dimen name="bubble_menu_padding">16dp</dimen> + <!-- Size of the icons in the manage menu. --> + <dimen name="bubble_menu_icon_size">24dp</dimen> <!-- Max width of the message bubble--> <dimen name="bubble_message_max_width">144dp</dimen> <!-- Min width of the message bubble --> @@ -174,17 +197,21 @@ <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> <dimen name="bubble_manage_menu_elevation">4dp</dimen> + <!-- Size of user education views on large screens (phone is just match parent). --> + <dimen name="bubbles_user_education_width_large_screen">400dp</dimen> + + <!-- Bottom and end margin for compat buttons. --> + <dimen name="compat_button_margin">16dp</dimen> + + <!-- The radius of the corners of the compat hint bubble. --> + <dimen name="compat_hint_corner_radius">28dp</dimen> - <!-- Bubbles user education views --> - <dimen name="bubbles_manage_education_width">160dp</dimen> - <!-- The inset from the top bound of the manage button to place the user education. --> - <dimen name="bubbles_manage_education_top_inset">65dp</dimen> - <!-- Size of padding for the user education cling, this should at minimum be larger than - individual_bubble_size + some padding. --> - <dimen name="bubble_stack_user_education_side_inset">72dp</dimen> + <!-- The width of the compat hint point. --> + <dimen name="compat_hint_point_width">10dp</dimen> - <!-- The width/height of the size compat restart button. --> - <dimen name="size_compat_button_size">48dp</dimen> + <!-- The end padding for the compat hint. Computed as (compat button width (=48) / 2 + + compat_button_margin - compat_hint_corner_radius - compat_hint_point_width / 2). --> + <dimen name="compat_hint_padding_end">7dp</dimen> <!-- The width of the brand image on staring surface. --> <dimen name="starting_surface_brand_image_width">200dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index e512698ab66c..c88fc16e218e 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -24,6 +24,9 @@ <!-- Label for PIP settings button [CHAR LIMIT=NONE]--> <string name="pip_phone_settings">Settings</string> + <!-- Label for the PIP enter split button [CHAR LIMIT=NONE] --> + <string name="pip_phone_enter_split">Enter split screen</string> + <!-- Title of menu shown over picture-in-picture. Used for accessibility. --> <string name="pip_menu_title">Menu</string> @@ -155,7 +158,4 @@ <!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] --> <string name="restart_button_description">Tap to restart this app and go full screen.</string> - - <!-- Generic "got it" acceptance of dialog or cling [CHAR LIMIT=NONE] --> - <string name="got_it">Got it</string> </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index fffcd33f7992..7733201d2465 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -32,8 +32,9 @@ <style name="DockedDividerBackground"> <item name="android:layout_width">match_parent</item> - <item name="android:layout_height">10dp</item> + <item name="android:layout_height">@dimen/split_divider_bar_width</item> <item name="android:layout_gravity">center_vertical</item> + <item name="android:background">@color/split_divider_background</item> </style> <style name="DockedDividerMinimizedShadow"> @@ -42,7 +43,7 @@ </style> <style name="DockedDividerHandle"> - <item name="android:layout_gravity">center_horizontal</item> + <item name="android:layout_gravity">center</item> <item name="android:layout_width">96dp</item> <item name="android:layout_height">48dp</item> </style> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java deleted file mode 100644 index 006730d333eb..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2020 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; - -import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; -import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; - -import android.app.ActivityManager; -import android.graphics.Point; -import android.util.Slog; -import android.util.SparseArray; -import android.view.SurfaceControl; - -import androidx.annotation.NonNull; - -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.common.SyncTransactionQueue; -import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.transition.Transitions; - -import java.io.PrintWriter; - -/** - * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}. - */ -public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { - private static final String TAG = "FullscreenTaskListener"; - - private final SyncTransactionQueue mSyncQueue; - - private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>(); - - public FullscreenTaskListener(SyncTransactionQueue syncQueue) { - mSyncQueue = syncQueue; - } - - @Override - public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { - if (mDataByTaskId.get(taskInfo.taskId) != null) { - throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", - taskInfo.taskId); - final Point positionInParent = taskInfo.positionInParent; - mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent)); - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - mSyncQueue.runInSync(t -> { - // Reset several properties back to fullscreen (PiP, for example, leaves all these - // properties in a bad state). - t.setWindowCrop(leash, null); - t.setPosition(leash, positionInParent.x, positionInParent.y); - t.setAlpha(leash, 1f); - t.setMatrix(leash, 1, 0, 0, 1); - t.show(leash); - }); - } - - @Override - public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - if (Transitions.ENABLE_SHELL_TRANSITIONS) return; - final TaskData data = mDataByTaskId.get(taskInfo.taskId); - final Point positionInParent = taskInfo.positionInParent; - if (!positionInParent.equals(data.positionInParent)) { - data.positionInParent.set(positionInParent.x, positionInParent.y); - mSyncQueue.runInSync(t -> { - t.setPosition(data.surface, positionInParent.x, positionInParent.y); - }); - } - } - - @Override - public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - if (mDataByTaskId.get(taskInfo.taskId) == null) { - Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); - return; - } - mDataByTaskId.remove(taskInfo.taskId); - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", - taskInfo.taskId); - } - - @Override - public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { - if (!mDataByTaskId.contains(taskId)) { - throw new IllegalArgumentException("There is no surface for taskId=" + taskId); - } - b.setParent(mDataByTaskId.get(taskId).surface); - } - - @Override - public void dump(@NonNull PrintWriter pw, String prefix) { - final String innerPrefix = prefix + " "; - pw.println(prefix + this); - pw.println(innerPrefix + mDataByTaskId.size() + " Tasks"); - } - - @Override - public String toString() { - return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); - } - - /** - * Per-task data for each managed task. - */ - private static class TaskData { - public final SurfaceControl surface; - public final Point positionInParent; - - public TaskData(SurfaceControl surface, Point positionInParent) { - this.surface = surface; - this.positionInParent = positionInParent; - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java new file mode 100644 index 000000000000..14ba9df93f24 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootDisplayAreaOrganizer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 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; + +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.window.DisplayAreaAppearedInfo; +import android.window.DisplayAreaInfo; +import android.window.DisplayAreaOrganizer; + +import androidx.annotation.NonNull; + +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.Executor; + +/** Display area organizer for the root display areas */ +public class RootDisplayAreaOrganizer extends DisplayAreaOrganizer { + + private static final String TAG = RootDisplayAreaOrganizer.class.getSimpleName(); + + /** {@link DisplayAreaInfo} list, which is mapped by display IDs. */ + private final SparseArray<DisplayAreaInfo> mDisplayAreasInfo = new SparseArray<>(); + /** Display area leashes, which is mapped by display IDs. */ + private final SparseArray<SurfaceControl> mLeashes = new SparseArray<>(); + + public RootDisplayAreaOrganizer(Executor executor) { + super(executor); + List<DisplayAreaAppearedInfo> infos = registerOrganizer(FEATURE_ROOT); + for (int i = infos.size() - 1; i >= 0; --i) { + onDisplayAreaAppeared(infos.get(i).getDisplayAreaInfo(), infos.get(i).getLeash()); + } + } + + public void attachToDisplayArea(int displayId, SurfaceControl.Builder b) { + final SurfaceControl sc = mLeashes.get(displayId); + if (sc != null) { + b.setParent(sc); + } + } + + @Override + public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, + @NonNull SurfaceControl leash) { + if (displayAreaInfo.featureId != FEATURE_ROOT) { + throw new IllegalArgumentException( + "Unknown feature: " + displayAreaInfo.featureId + + "displayAreaInfo:" + displayAreaInfo); + } + + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) != null) { + throw new IllegalArgumentException( + "Duplicate DA for displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.put(displayId, displayAreaInfo); + mLeashes.put(displayId, leash); + } + + @Override + public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) == null) { + throw new IllegalArgumentException( + "onDisplayAreaVanished() Unknown DA displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.remove(displayId); + } + + @Override + public void onDisplayAreaInfoChanged(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + if (mDisplayAreasInfo.get(displayId) == null) { + throw new IllegalArgumentException( + "onDisplayAreaInfoChanged() Unknown DA displayId: " + displayId + + " displayAreaInfo:" + displayAreaInfo + + " mDisplayAreasInfo.get():" + mDisplayAreasInfo.get(displayId)); + } + + mDisplayAreasInfo.put(displayId, displayAreaInfo); + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } + + @Override + public String toString() { + return TAG + "#" + mDisplayAreasInfo.size(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 34c66a4f4b82..bf074b0337ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -97,6 +97,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { b.setParent(sc); } + public void setPosition(@NonNull SurfaceControl.Transaction tx, int displayId, int x, int y) { + final SurfaceControl sc = mLeashes.get(displayId); + if (sc == null) { + throw new IllegalArgumentException("can't find display" + displayId); + } + tx.setPosition(sc, x, y); + } + @Override public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo, @NonNull SurfaceControl leash) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java index 0b941b59b3db..908a31dc3e4e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellCommandHandlerImpl.java @@ -16,7 +16,7 @@ package com.android.wm.shell; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.common.ShellExecutor; @@ -24,6 +24,7 @@ import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; @@ -43,6 +44,7 @@ public final class ShellCommandHandlerImpl { private final Optional<OneHandedController> mOneHandedOptional; private final Optional<HideDisplayCutoutController> mHideDisplayCutout; private final Optional<AppPairsController> mAppPairsOptional; + private final Optional<RecentTasksController> mRecentTasks; private final ShellTaskOrganizer mShellTaskOrganizer; private final ShellExecutor mMainExecutor; private final HandlerImpl mImpl = new HandlerImpl(); @@ -55,8 +57,10 @@ public final class ShellCommandHandlerImpl { Optional<OneHandedController> oneHandedOptional, Optional<HideDisplayCutoutController> hideDisplayCutout, Optional<AppPairsController> appPairsOptional, + Optional<RecentTasksController> recentTasks, ShellExecutor mainExecutor) { mShellTaskOrganizer = shellTaskOrganizer; + mRecentTasks = recentTasks; mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; mPipOptional = pipOptional; @@ -85,6 +89,9 @@ public final class ShellCommandHandlerImpl { pw.println(); pw.println(); mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, "")); + pw.println(); + pw.println(); + mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, "")); } @@ -175,7 +182,7 @@ public final class ShellCommandHandlerImpl { private boolean runSetSideStageVisibility(String[] args, PrintWriter pw) { if (args.length < 3) { // First arguments are "WMShell" and command name. - pw.println("Error: side stage position should be provided as arguments"); + pw.println("Error: side stage visibility should be provided as arguments"); return false; } final Boolean visible = new Boolean(args[2]); @@ -197,6 +204,8 @@ public final class ShellCommandHandlerImpl { pw.println(" Move a task with given id in split-screen mode."); pw.println(" removeFromSideStage <taskId>"); pw.println(" Remove a task with given id in split-screen mode."); + pw.println(" setSideStageOutline <true/false>"); + pw.println(" Enable/Disable outline on the side-stage."); pw.println(" setSideStagePosition <SideStagePosition>"); pw.println(" Sets the position of the side-stage."); pw.println(" setSideStageVisibility <true/false>"); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java index d1fbf31e2b99..c3ce3627fb0b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellInitImpl.java @@ -20,12 +20,17 @@ import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCR import com.android.wm.shell.apppairs.AppPairsController; import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ExternalThread; import com.android.wm.shell.draganddrop.DragAndDropController; -import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.fullscreen.FullscreenUnfoldController; import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.startingsurface.StartingWindowController; import com.android.wm.shell.transition.Transitions; @@ -38,42 +43,55 @@ import java.util.Optional; public class ShellInitImpl { private static final String TAG = ShellInitImpl.class.getSimpleName(); + private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private final DragAndDropController mDragAndDropController; private final ShellTaskOrganizer mShellTaskOrganizer; private final Optional<BubbleController> mBubblesOptional; - private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; private final Optional<SplitScreenController> mSplitScreenOptional; private final Optional<AppPairsController> mAppPairsOptional; private final Optional<PipTouchHandler> mPipTouchHandlerOptional; private final FullscreenTaskListener mFullscreenTaskListener; + private final Optional<FullscreenUnfoldController> mFullscreenUnfoldController; + private final Optional<FreeformTaskListener> mFreeformTaskListenerOptional; private final ShellExecutor mMainExecutor; private final Transitions mTransitions; private final StartingWindowController mStartingWindow; + private final Optional<RecentTasksController> mRecentTasks; private final InitImpl mImpl = new InitImpl(); - public ShellInitImpl(DisplayImeController displayImeController, + public ShellInitImpl( + DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, Optional<BubbleController> bubblesOptional, - Optional<LegacySplitScreenController> legacySplitScreenOptional, Optional<SplitScreenController> splitScreenOptional, Optional<AppPairsController> appPairsOptional, Optional<PipTouchHandler> pipTouchHandlerOptional, FullscreenTaskListener fullscreenTaskListener, + Optional<FullscreenUnfoldController> fullscreenUnfoldTransitionController, + Optional<FreeformTaskListener> freeformTaskListenerOptional, + Optional<RecentTasksController> recentTasks, Transitions transitions, StartingWindowController startingWindow, ShellExecutor mainExecutor) { + mDisplayController = displayController; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mDragAndDropController = dragAndDropController; mShellTaskOrganizer = shellTaskOrganizer; mBubblesOptional = bubblesOptional; - mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; mAppPairsOptional = appPairsOptional; mFullscreenTaskListener = fullscreenTaskListener; mPipTouchHandlerOptional = pipTouchHandlerOptional; + mFullscreenUnfoldController = fullscreenUnfoldTransitionController; + mFreeformTaskListenerOptional = freeformTaskListenerOptional; + mRecentTasks = recentTasks; mTransitions = transitions; mMainExecutor = mainExecutor; mStartingWindow = startingWindow; @@ -84,7 +102,9 @@ public class ShellInitImpl { } private void init() { - // Start listening for display changes + // Start listening for display and insets changes + mDisplayController.initialize(); + mDisplayInsetsController.initialize(); mDisplayImeController.startMonitorDisplays(); // Setup the shell organizer @@ -108,6 +128,14 @@ public class ShellInitImpl { // controller instead of the feature interface, can just initialize the touch handler if // needed mPipTouchHandlerOptional.ifPresent((handler) -> handler.init()); + + // Initialize optional freeform + mFreeformTaskListenerOptional.ifPresent(f -> + mShellTaskOrganizer.addListenerForType( + f, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM)); + + mFullscreenUnfoldController.ifPresent(FullscreenUnfoldController::init); + mRecentTasks.ifPresent(RecentTasksController::init); } @ExternalThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 656bdff0c782..8b3a35688f11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -29,8 +29,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; +import android.app.WindowConfiguration; import android.content.Context; import android.content.LocusId; +import android.content.pm.ActivityInfo; import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; @@ -41,14 +43,17 @@ import android.util.SparseArray; import android.view.SurfaceControl; import android.window.ITaskOrganizerController; import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; import android.window.TaskOrganizer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.util.FrameworkStatsLog; import com.android.wm.shell.common.ScreenshotUtils; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.sizecompatui.SizeCompatUIController; +import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.startingsurface.StartingWindowController; import java.io.PrintWriter; @@ -56,6 +61,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.function.Consumer; /** @@ -63,7 +69,7 @@ import java.util.function.Consumer; * TODO(b/167582004): may consider consolidating this class and TaskOrganizer */ public class ShellTaskOrganizer extends TaskOrganizer implements - SizeCompatUIController.SizeCompatUICallback { + CompatUIController.CompatUICallback { // Intentionally using negative numbers here so the positive numbers can be used // for task id specific listeners that will be added later. @@ -71,12 +77,14 @@ public class ShellTaskOrganizer extends TaskOrganizer implements public static final int TASK_LISTENER_TYPE_FULLSCREEN = -2; public static final int TASK_LISTENER_TYPE_MULTI_WINDOW = -3; public static final int TASK_LISTENER_TYPE_PIP = -4; + public static final int TASK_LISTENER_TYPE_FREEFORM = -5; @IntDef(prefix = {"TASK_LISTENER_TYPE_"}, value = { TASK_LISTENER_TYPE_UNDEFINED, TASK_LISTENER_TYPE_FULLSCREEN, TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_PIP, + TASK_LISTENER_TYPE_FREEFORM, }) public @interface TaskListenerType {} @@ -90,9 +98,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements default void onTaskInfoChanged(RunningTaskInfo taskInfo) {} default void onTaskVanished(RunningTaskInfo taskInfo) {} default void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) {} - /** Whether this task listener supports size compat UI. */ - default boolean supportSizeCompatUI() { - // All TaskListeners should support size compat except PIP. + /** Whether this task listener supports compat UI. */ + default boolean supportCompatUI() { + // All TaskListeners should support compat UI except PIP. return true; } /** Attaches the a child window surface to the task surface. */ @@ -115,6 +123,16 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } /** + * Callbacks for events in which the focus has changed. + */ + public interface FocusListener { + /** + * Notifies when the task which is focused has changed. + */ + void onFocusTaskChanged(RunningTaskInfo taskInfo); + } + + /** * Keys map from either a task id or {@link TaskListenerType}. * @see #addListenerForTaskId * @see #addListenerForType @@ -135,32 +153,51 @@ public class ShellTaskOrganizer extends TaskOrganizer implements /** @see #addLocusIdListener */ private final ArraySet<LocusIdListener> mLocusIdListeners = new ArraySet<>(); + private final ArraySet<FocusListener> mFocusListeners = new ArraySet<>(); + private final Object mLock = new Object(); private StartingWindowController mStartingWindow; /** - * In charge of showing size compat UI. Can be {@code null} if device doesn't support size + * In charge of showing compat UI. Can be {@code null} if device doesn't support size * compat. */ @Nullable - private final SizeCompatUIController mSizeCompatUI; + private final CompatUIController mCompatUI; + + @Nullable + private final Optional<RecentTasksController> mRecentTasks; + + @Nullable + private RunningTaskInfo mLastFocusedTaskInfo; public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) { - this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */); + this(null /* taskOrganizerController */, mainExecutor, context, null /* compatUI */, + Optional.empty() /* recentTasksController */); + } + + public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable + CompatUIController compatUI) { + this(null /* taskOrganizerController */, mainExecutor, context, compatUI, + Optional.empty() /* recentTasksController */); } public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable - SizeCompatUIController sizeCompatUI) { - this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI); + CompatUIController compatUI, + Optional<RecentTasksController> recentTasks) { + this(null /* taskOrganizerController */, mainExecutor, context, compatUI, + recentTasks); } @VisibleForTesting ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor, - Context context, @Nullable SizeCompatUIController sizeCompatUI) { + Context context, @Nullable CompatUIController compatUI, + Optional<RecentTasksController> recentTasks) { super(taskOrganizerController, mainExecutor); - mSizeCompatUI = sizeCompatUI; - if (sizeCompatUI != null) { - sizeCompatUI.setSizeCompatUICallback(this); + mCompatUI = compatUI; + mRecentTasks = recentTasks; + if (compatUI != null) { + compatUI.setCompatUICallback(this); } } @@ -179,6 +216,14 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + @Override + public void unregisterOrganizer() { + super.unregisterOrganizer(); + if (mStartingWindow != null) { + mStartingWindow.clearAllWindows(); + } + } + public void createRootTask(int displayId, int windowingMode, TaskListener listener) { ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", displayId, windowingMode, listener.toString()); @@ -229,14 +274,14 @@ public class ShellTaskOrganizer extends TaskOrganizer implements + " already exists"); } mTaskListeners.put(listenerType, listener); + } - // Notify the listener of all existing tasks with the given type. - for (int i = mTasks.size() - 1; i >= 0; --i) { - final TaskAppearedInfo data = mTasks.valueAt(i); - final TaskListener taskListener = getTaskListener(data.getTaskInfo()); - if (taskListener != listener) continue; - listener.onTaskAppeared(data.getTaskInfo(), data.getLeash()); - } + // Notify the listener of all existing tasks with the given type. + for (int i = mTasks.size() - 1; i >= 0; --i) { + final TaskAppearedInfo data = mTasks.valueAt(i); + final TaskListener taskListener = getTaskListener(data.getTaskInfo()); + if (taskListener != listener) continue; + listener.onTaskAppeared(data.getTaskInfo(), data.getLeash()); } } } @@ -262,8 +307,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements tasks.add(data); } - // Remove listener - mTaskListeners.removeAt(index); + // Remove listener, there can be the multiple occurrences, so search the whole list. + for (int i = mTaskListeners.size() - 1; i >= 0; --i) { + if (mTaskListeners.valueAt(i) == listener) { + mTaskListeners.removeAt(i); + } + } // Associate tasks with new listeners if needed. for (int i = tasks.size() - 1; i >= 0; --i) { @@ -306,6 +355,27 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Adds a listener to be notified for task focus changes. + */ + public void addFocusListener(FocusListener listener) { + synchronized (mLock) { + mFocusListeners.add(listener); + if (mLastFocusedTaskInfo != null) { + listener.onFocusTaskChanged(mLastFocusedTaskInfo); + } + } + } + + /** + * Removes listener. + */ + public void removeLocusIdListener(FocusListener listener) { + synchronized (mLock) { + mFocusListeners.remove(listener); + } + } + @Override public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { if (mStartingWindow != null) { @@ -314,10 +384,9 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override - public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { if (mStartingWindow != null) { - mStartingWindow.removeStartingWindow(taskId, leash, frame, playRevealAnimation); + mStartingWindow.removeStartingWindow(removalInfo); } } @@ -359,7 +428,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements listener.onTaskAppeared(info.getTaskInfo(), info.getLeash()); } notifyLocusVisibilityIfNeeded(info.getTaskInfo()); - notifySizeCompatUI(info.getTaskInfo(), listener); + notifyCompatUI(info.getTaskInfo(), listener); } /** @@ -390,8 +459,25 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } notifyLocusVisibilityIfNeeded(taskInfo); if (updated || !taskInfo.equalsForSizeCompat(data.getTaskInfo())) { - // Notify the size compat UI if the listener or task info changed. - notifySizeCompatUI(taskInfo, newListener); + // Notify the compat UI if the listener or task info changed. + notifyCompatUI(taskInfo, newListener); + } + if (data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode()) { + // Notify the recent tasks when a task changes windowing modes + mRecentTasks.ifPresent(recentTasks -> + recentTasks.onTaskWindowingModeChanged(taskInfo)); + } + // TODO (b/207687679): Remove check for HOME once bug is fixed + final boolean isFocusedOrHome = taskInfo.isFocused + || (taskInfo.topActivityType == WindowConfiguration.ACTIVITY_TYPE_HOME + && taskInfo.isVisible); + final boolean focusTaskChanged = (mLastFocusedTaskInfo == null + || mLastFocusedTaskInfo.taskId != taskInfo.taskId) && isFocusedOrHome; + if (focusTaskChanged) { + for (int i = 0; i < mFocusListeners.size(); i++) { + mFocusListeners.valueAt(i).onFocusTaskChanged(taskInfo); + } + mLastFocusedTaskInfo = taskInfo; } } } @@ -418,8 +504,10 @@ public class ShellTaskOrganizer extends TaskOrganizer implements listener.onTaskVanished(taskInfo); } notifyLocusVisibilityIfNeeded(taskInfo); - // Pass null for listener to remove the size compat UI on this task if there is any. - notifySizeCompatUI(taskInfo, null /* taskListener */); + // Pass null for listener to remove the compat UI on this task if there is any. + notifyCompatUI(taskInfo, null /* taskListener */); + // Notify the recent tasks that a task has been removed + mRecentTasks.ifPresent(recentTasks -> recentTasks.onTaskRemoved(taskInfo)); } } @@ -493,39 +581,65 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } @Override + public void onSizeCompatRestartButtonAppeared(int taskId) { + final TaskAppearedInfo info; + synchronized (mLock) { + info = mTasks.get(taskId); + } + if (info == null) { + return; + } + logSizeCompatRestartButtonEventReported(info, + FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__APPEARED); + } + + @Override public void onSizeCompatRestartButtonClicked(int taskId) { final TaskAppearedInfo info; synchronized (mLock) { info = mTasks.get(taskId); } - if (info != null) { - restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); + if (info == null) { + return; + } + logSizeCompatRestartButtonEventReported(info, + FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED__EVENT__CLICKED); + restartTaskTopActivityProcessIfVisible(info.getTaskInfo().token); + } + + private void logSizeCompatRestartButtonEventReported(@NonNull TaskAppearedInfo info, + int event) { + ActivityInfo topActivityInfo = info.getTaskInfo().topActivityInfo; + if (topActivityInfo == null) { + return; } + FrameworkStatsLog.write(FrameworkStatsLog.SIZE_COMPAT_RESTART_BUTTON_EVENT_REPORTED, + topActivityInfo.applicationInfo.uid, event); } /** - * Notifies {@link SizeCompatUIController} about the size compat info changed on the give Task + * Notifies {@link CompatUIController} about the compat info changed on the give Task * to update the UI accordingly. * * @param taskInfo the new Task info * @param taskListener listener to handle the Task Surface placement. {@code null} if task is * vanished. */ - private void notifySizeCompatUI(RunningTaskInfo taskInfo, @Nullable TaskListener taskListener) { - if (mSizeCompatUI == null) { + private void notifyCompatUI(RunningTaskInfo taskInfo, @Nullable TaskListener taskListener) { + if (mCompatUI == null) { return; } - // The task is vanished or doesn't support size compat UI, notify to remove size compat UI + // The task is vanished or doesn't support compat UI, notify to remove compat UI // on this Task if there is any. - if (taskListener == null || !taskListener.supportSizeCompatUI() + if (taskListener == null || !taskListener.supportCompatUI() || !taskInfo.topActivityInSizeCompat || !taskInfo.isVisible) { - mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, + mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, null /* taskConfig */, null /* taskListener */); return; } - mSizeCompatUI.onSizeCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, + mCompatUI.onCompatInfoChanged(taskInfo.displayId, taskInfo.taskId, taskInfo.configuration, taskListener); } @@ -579,6 +693,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements case WINDOWING_MODE_PINNED: return TASK_LISTENER_TYPE_PIP; case WINDOWING_MODE_FREEFORM: + return TASK_LISTENER_TYPE_FREEFORM; case WINDOWING_MODE_UNDEFINED: default: return TASK_LISTENER_TYPE_UNDEFINED; @@ -593,6 +708,8 @@ public class ShellTaskOrganizer extends TaskOrganizer implements return "TASK_LISTENER_TYPE_MULTI_WINDOW"; case TASK_LISTENER_TYPE_PIP: return "TASK_LISTENER_TYPE_PIP"; + case TASK_LISTENER_TYPE_FREEFORM: + return "TASK_LISTENER_TYPE_FREEFORM"; case TASK_LISTENER_TYPE_UNDEFINED: return "TASK_LISTENER_TYPE_UNDEFINED"; default: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index 1861e48482b8..2f3214d1d1ab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -40,6 +40,8 @@ import android.view.ViewTreeObserver; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.wm.shell.common.SyncTransactionQueue; + import java.io.PrintWriter; import java.util.concurrent.Executor; @@ -74,6 +76,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final ShellTaskOrganizer mTaskOrganizer; private final Executor mShellExecutor; + private final SyncTransactionQueue mSyncQueue; private ActivityManager.RunningTaskInfo mTaskInfo; private WindowContainerToken mTaskToken; @@ -89,11 +92,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private final Rect mTmpRootRect = new Rect(); private final int[] mTmpLocation = new int[2]; - public TaskView(Context context, ShellTaskOrganizer organizer) { + public TaskView(Context context, ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue) { super(context, null, 0, 0, true /* disableBackgroundLayer */); mTaskOrganizer = organizer; mShellExecutor = organizer.getExecutor(); + mSyncQueue = syncQueue; setUseAlpha(); getHolder().addCallback(this); mGuard.open("release"); @@ -189,8 +193,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mTaskToken, mTmpRect); - // TODO(b/151449487): Enable synchronization - mTaskOrganizer.applyTransaction(wct); + mSyncQueue.queue(wct); } /** @@ -236,14 +239,16 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, private void updateTaskVisibility() { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setHidden(mTaskToken, !mSurfaceCreated /* hidden */); - mTaskOrganizer.applyTransaction(wct); - // TODO(b/151449487): Only call callback once we enable synchronization - if (mListener != null) { - final int taskId = mTaskInfo.taskId; + mSyncQueue.queue(wct); + if (mListener == null) { + return; + } + int taskId = mTaskInfo.taskId; + mSyncQueue.runInSync((t) -> { mListenerExecutor.execute(() -> { mListener.onTaskVisibilityChanged(taskId, mSurfaceCreated); }); - } + }); } @Override @@ -264,10 +269,12 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, updateTaskVisibility(); } mTaskOrganizer.setInterceptBackPressedOnTaskRoot(mTaskToken, true); - // TODO: Synchronize show with the resize onLocationChanged(); if (taskInfo.taskDescription != null) { - setResizeBackgroundColor(taskInfo.taskDescription.getBackgroundColor()); + int backgroundColor = taskInfo.taskDescription.getBackgroundColor(); + mSyncQueue.runInSync((t) -> { + setResizeBackgroundColor(t, backgroundColor); + }); } if (mListener != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java index 58ca1fbaba24..8286d102791e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java @@ -20,8 +20,8 @@ import android.annotation.UiContext; import android.content.Context; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.annotations.ShellMainThread; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -30,12 +30,14 @@ import java.util.function.Consumer; public class TaskViewFactoryController { private final ShellTaskOrganizer mTaskOrganizer; private final ShellExecutor mShellExecutor; + private final SyncTransactionQueue mSyncQueue; private final TaskViewFactory mImpl = new TaskViewFactoryImpl(); public TaskViewFactoryController(ShellTaskOrganizer taskOrganizer, - ShellExecutor shellExecutor) { + ShellExecutor shellExecutor, SyncTransactionQueue syncQueue) { mTaskOrganizer = taskOrganizer; mShellExecutor = shellExecutor; + mSyncQueue = syncQueue; } public TaskViewFactory asTaskViewFactory() { @@ -44,7 +46,7 @@ public class TaskViewFactoryController { /** Creates an {@link TaskView} */ public void create(@UiContext Context context, Executor executor, Consumer<TaskView> onCreate) { - TaskView taskView = new TaskView(context, mTaskOrganizer); + TaskView taskView = new TaskView(context, mTaskOrganizer, mSyncQueue); executor.execute(() -> { onCreate.accept(taskView); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 8aca01d2467b..2aead9392e59 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -62,4 +62,10 @@ public class Interpolators { */ public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f, 1); + + public static final PathInterpolator SLOWDOWN_INTERPOLATOR = + new PathInterpolator(0.5f, 1f, 0.5f, 1f); + + public static final PathInterpolator DIM_INTERPOLATOR = + new PathInterpolator(.23f, .87f, .52f, -0.11f); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java index e6d088e6537d..1f21937b5025 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPair.java @@ -19,14 +19,14 @@ package com.android.wm.shell.apppairs; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG; import android.app.ActivityManager; -import android.graphics.Rect; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerToken; @@ -39,9 +39,11 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SurfaceUtils; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.common.split.SplitWindowManager; import java.io.PrintWriter; @@ -67,13 +69,33 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou private final SyncTransactionQueue mSyncQueue; private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private SplitLayout mSplitLayout; + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = + new SplitWindowManager.ParentContainerCallbacks() { + @Override + public void attachToParentSurface(SurfaceControl.Builder b) { + b.setParent(mRootTaskLeash); + } + + @Override + public void onLeashReady(SurfaceControl leash) { + mSyncQueue.runInSync(t -> t + .show(leash) + .setLayer(leash, SPLIT_DIVIDER_LAYER) + .setPosition(leash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top)); + } + }; + AppPair(AppPairsController controller) { mController = controller; mSyncQueue = controller.getSyncTransactionQueue(); mDisplayController = controller.getDisplayController(); mDisplayImeController = controller.getDisplayImeController(); + mDisplayInsetsController = controller.getDisplayInsetsController(); } int getRootTaskId() { @@ -109,8 +131,9 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou mSplitLayout = new SplitLayout(TAG + "SplitDivider", mDisplayController.getDisplayContext(mRootTaskInfo.displayId), mRootTaskInfo.configuration, this /* layoutChangeListener */, - b -> b.setParent(mRootTaskLeash), mDisplayImeController, - mController.getTaskOrganizer()); + mParentContainerCallbacks, mDisplayImeController, mController.getTaskOrganizer(), + true /* applyDismissingParallax */); + mDisplayInsetsController.addInsetsChangedListener(mRootTaskInfo.displayId, mSplitLayout); final WindowContainerToken token1 = task1.token; final WindowContainerToken token2 = task2.token; @@ -176,21 +199,17 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou if (mTaskLeash1 == null || mTaskLeash2 == null) return; mSplitLayout.init(); - final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - final Rect dividerBounds = mSplitLayout.getDividerBounds(); - - // TODO: Is there more we need to do here? - mSyncQueue.runInSync(t -> { - t.setLayer(dividerLeash, Integer.MAX_VALUE) - .setPosition(mTaskLeash1, mTaskInfo1.positionInParent.x, - mTaskInfo1.positionInParent.y) - .setPosition(mTaskLeash2, mTaskInfo2.positionInParent.x, - mTaskInfo2.positionInParent.y) - .setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) - .show(mRootTaskLeash) - .show(mTaskLeash1) - .show(mTaskLeash2); - }); + + mSyncQueue.runInSync(t -> t + .show(mRootTaskLeash) + .show(mTaskLeash1) + .show(mTaskLeash2) + .setPosition(mTaskLeash1, + mTaskInfo1.positionInParent.x, + mTaskInfo1.positionInParent.y) + .setPosition(mTaskLeash2, + mTaskInfo2.positionInParent.x, + mTaskInfo2.positionInParent.y)); } @Override @@ -214,7 +233,7 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou if (mSplitLayout != null && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)) { - onBoundsChanged(mSplitLayout); + onLayoutSizeChanged(mSplitLayout); } } else if (taskInfo.taskId == getTaskId1()) { mTaskInfo1 = taskInfo; @@ -295,17 +314,30 @@ class AppPair implements ShellTaskOrganizer.TaskListener, SplitLayout.SplitLayou } @Override - public void onBoundsChanging(SplitLayout layout) { + public void onLayoutPositionChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> + layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); + } + + @Override + public void onLayoutSizeChanging(SplitLayout layout) { mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); } @Override - public void onBoundsChanged(SplitLayout layout) { + public void onLayoutSizeChanged(SplitLayout layout) { final WindowContainerTransaction wct = new WindowContainerTransaction(); layout.applyTaskChanges(wct, mTaskInfo1, mTaskInfo2); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, mTaskLeash1, mTaskLeash2, mDimLayer1, mDimLayer2)); } + + @Override + public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, mTaskInfo1, mTaskInfo2); + mController.getTaskOrganizer().applyTransaction(wct); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java index b159333e9a0e..53234ab971d6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/AppPairsController.java @@ -29,6 +29,7 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -50,14 +51,17 @@ public class AppPairsController { private final SparseArray<AppPair> mActiveAppPairs = new SparseArray<>(); private final DisplayController mDisplayController; private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; public AppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, DisplayController displayController, ShellExecutor mainExecutor, - DisplayImeController displayImeController) { + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController) { mTaskOrganizer = organizer; mSyncQueue = syncQueue; mDisplayController = displayController; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; } @@ -148,6 +152,10 @@ public class AppPairsController { return mDisplayImeController; } + DisplayInsetsController getDisplayInsetsController() { + return mDisplayInsetsController; + } + public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; final String childPrefix = innerPrefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS new file mode 100644 index 000000000000..4d9b520e3f0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/apppairs/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-modules apppair owner +chenghsiuchang@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 9d65d28b21b4..8d43f1375a8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -121,7 +121,7 @@ public class Bubble implements BubbleViewProvider { @Nullable private Icon mIcon; private boolean mIsBubble; - private boolean mIsVisuallyInterruptive; + private boolean mIsTextChanged; private boolean mIsClearable; private boolean mShouldSuppressNotificationDot; private boolean mShouldSuppressNotificationList; @@ -342,12 +342,12 @@ public class Bubble implements BubbleViewProvider { } /** - * Sets whether this bubble is considered visually interruptive. This method is purely for + * Sets whether this bubble is considered text changed. This method is purely for * testing. */ @VisibleForTesting - void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { - mIsVisuallyInterruptive = visuallyInterruptive; + void setTextChangedForTest(boolean textChanged) { + mIsTextChanged = textChanged; } /** @@ -422,14 +422,6 @@ public class Bubble implements BubbleViewProvider { } } - @Override - public void setExpandedContentAlpha(float alpha) { - if (mExpandedView != null) { - mExpandedView.setAlpha(alpha); - mExpandedView.setTaskViewAlpha(alpha); - } - } - /** * Set visibility of bubble in the expanded state. * @@ -462,7 +454,7 @@ public class Bubble implements BubbleViewProvider { mFlyoutMessage = extractFlyoutMessage(entry); if (entry.getRanking() != null) { mShortcutInfo = entry.getRanking().getConversationShortcutInfo(); - mIsVisuallyInterruptive = entry.getRanking().visuallyInterruptive(); + mIsTextChanged = entry.getRanking().isTextChanged(); if (entry.getRanking().getChannel() != null) { mIsImportantConversation = entry.getRanking().getChannel().isImportantConversation(); @@ -503,8 +495,8 @@ public class Bubble implements BubbleViewProvider { return mIcon; } - boolean isVisuallyInterruptive() { - return mIsVisuallyInterruptive; + boolean isTextChanged() { + return mIsTextChanged; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 09fcb86e56de..ec59fad3e95b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -56,7 +56,6 @@ import android.graphics.Rect; import android.os.Binder; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -70,6 +69,7 @@ import android.util.SparseArray; import android.util.SparseSetArray; import android.view.View; import android.view.ViewGroup; +import android.view.WindowInsets; import android.view.WindowManager; import android.window.WindowContainerTransaction; @@ -85,6 +85,7 @@ import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.pip.PinnedStackListenerForwarder; @@ -97,7 +98,6 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -111,8 +111,7 @@ public class BubbleController { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; - // TODO(b/173386799) keep in sync with Launcher3 and also don't do a broadcast - public static final String TASKBAR_CHANGED_BROADCAST = "taskbarChanged"; + // TODO(b/173386799) keep in sync with Launcher3, not hooked up to anything public static final String EXTRA_TASKBAR_CREATED = "taskbarCreated"; public static final String EXTRA_BUBBLE_OVERFLOW_OPENED = "bubbleOverflowOpened"; public static final String EXTRA_TASKBAR_VISIBLE = "taskbarVisible"; @@ -137,6 +136,7 @@ public class BubbleController { private final TaskStackListenerImpl mTaskStackListener; private final ShellTaskOrganizer mTaskOrganizer; private final DisplayController mDisplayController; + private final SyncTransactionQueue mSyncQueue; // Used to post to main UI thread private final ShellExecutor mMainExecutor; @@ -144,7 +144,6 @@ public class BubbleController { private BubbleLogger mLogger; private BubbleData mBubbleData; - private View mBubbleScrim; @Nullable private BubbleStackView mStackView; private BubbleIconFactory mBubbleIconFactory; private BubblePositioner mBubblePositioner; @@ -189,6 +188,9 @@ public class BubbleController { /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */ private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED; + /** Saved insets, used to detect WindowInset changes. */ + private WindowInsets mWindowInsets; + private boolean mInflateSynchronously; /** True when user is in status bar unlock shade. */ @@ -209,7 +211,8 @@ public class BubbleController { ShellTaskOrganizer organizer, DisplayController displayController, ShellExecutor mainExecutor, - Handler mainHandler) { + Handler mainHandler, + SyncTransactionQueue syncQueue) { BubbleLogger logger = new BubbleLogger(uiEventLogger); BubblePositioner positioner = new BubblePositioner(context, windowManager); BubbleData data = new BubbleData(context, logger, positioner, mainExecutor); @@ -217,7 +220,7 @@ public class BubbleController { new BubbleDataRepository(context, launcherApps, mainExecutor), statusBarService, windowManager, windowManagerShellWrapper, launcherApps, logger, taskStackListener, organizer, positioner, displayController, mainExecutor, - mainHandler); + mainHandler, syncQueue); } /** @@ -239,7 +242,8 @@ public class BubbleController { BubblePositioner positioner, DisplayController displayController, ShellExecutor mainExecutor, - Handler mainHandler) { + Handler mainHandler, + SyncTransactionQueue syncQueue) { mContext = context; mLauncherApps = launcherApps; mBarService = statusBarService == null @@ -262,6 +266,7 @@ public class BubbleController { mSavedBubbleKeysPerUser = new SparseSetArray<>(); mBubbleIconFactory = new BubbleIconFactory(context); mDisplayController = displayController; + mSyncQueue = syncQueue; } public void initialize() { @@ -561,6 +566,10 @@ public class BubbleController { return mTaskOrganizer; } + SyncTransactionQueue getSyncTransactionQueue() { + return mSyncQueue; + } + /** Contains information to help position things on the screen. */ BubblePositioner getPositioner() { return mBubblePositioner; @@ -572,7 +581,7 @@ public class BubbleController { /** * BubbleStackView is lazily created by this method the first time a Bubble is added. This - * method initializes the stack view and adds it to the StatusBar just above the scrim. + * method initializes the stack view and adds it to window manager. */ private void ensureStackViewCreated() { if (mStackView == null) { @@ -620,20 +629,31 @@ public class BubbleController { try { mAddedToWindowManager = true; mBubbleData.getOverflow().initialize(this); - mStackView.addView(mBubbleScrim); mWindowManager.addView(mStackView, mWmLayoutParams); - // Position info is dependent on us being attached to a window - mBubblePositioner.update(); + mStackView.setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + mBubblePositioner.update(); + mStackView.onDisplaySizeChanged(); + } + return windowInsets; + }); } catch (IllegalStateException e) { // This means the stack has already been added. This shouldn't happen... e.printStackTrace(); } } - /** For the overflow to be focusable & receive key events the flags must be update. **/ - void updateWindowFlagsForOverflow(boolean showingOverflow) { + /** + * In some situations bubble's should be able to receive key events for back: + * - when the bubble overflow is showing + * - when the user education for the stack is showing. + * + * @param interceptBack whether back should be intercepted or not. + */ + void updateWindowFlagsForBackpress(boolean interceptBack) { if (mStackView != null && mAddedToWindowManager) { - mWmLayoutParams.flags = showingOverflow + mWmLayoutParams.flags = interceptBack ? 0 : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; @@ -652,7 +672,6 @@ public class BubbleController { mAddedToWindowManager = false; if (mStackView != null) { mWindowManager.removeView(mStackView); - mStackView.removeView(mBubbleScrim); mBubbleData.getOverflow().cleanUpExpandedState(); } else { Log.w(TAG, "StackView added to WindowManager, but was null when removing!"); @@ -754,13 +773,6 @@ public class BubbleController { } } - private void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) { - mBubbleScrim = view; - callback.accept(mMainExecutor, mMainExecutor.executeBlockingForResult(() -> { - return Looper.myLooper(); - }, Looper.class)); - } - private void setSysuiProxy(Bubbles.SysuiProxy proxy) { mSysuiProxy = proxy; } @@ -897,8 +909,7 @@ public class BubbleController { * Fills the overflow bubbles by loading them from disk. */ void loadOverflowBubblesFromDisk() { - if (!mBubbleData.getOverflowBubbles().isEmpty() && !mOverflowDataLoadNeeded) { - // we don't need to load overflow bubbles from disk if it is already in memory + if (!mOverflowDataLoadNeeded) { return; } mOverflowDataLoadNeeded = false; @@ -927,7 +938,7 @@ public class BubbleController { public void updateBubble(BubbleEntry notif, boolean suppressFlyout, boolean showInShade) { // If this is an interruptive notif, mark that it's interrupted mSysuiProxy.setNotificationInterruption(notif.getKey()); - if (!notif.getRanking().visuallyInterruptive() + if (!notif.getRanking().isTextChanged() && (notif.getBubbleMetadata() != null && !notif.getBubbleMetadata().getAutoExpandBubble()) && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { @@ -1016,15 +1027,17 @@ public class BubbleController { // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. // This means that the app or channel's ability to bubble has been revoked. mBubbleData.dismissBubbleWithKey(key, DISMISS_BLOCKED); - } else if (isActiveBubble && !shouldBubbleUp) { - // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it. - // This happens when DND is enabled and configured to hide bubbles. Dismissing with - // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that - // the bubble will be re-created if shouldBubbleUp returns true. + } else if (isActiveBubble && (!shouldBubbleUp || entry.getRanking().isSuspended())) { + // If this entry is allowed to bubble, but cannot currently bubble up or is + // suspended, dismiss it. This happens when DND is enabled and configured to hide + // bubbles, or focus mode is enabled and the app is designated as distracting. + // Dismissing with the reason DISMISS_NO_BUBBLE_UP will retain the underlying + // notification, so that the bubble will be re-created if shouldBubbleUp returns + // true. mBubbleData.dismissBubbleWithKey(key, DISMISS_NO_BUBBLE_UP); } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { entry.setFlagBubble(true); - onEntryUpdated(entry, true /* shouldBubbleUp */); + onEntryUpdated(entry, shouldBubbleUp && !entry.getRanking().isSuspended()); } } } @@ -1122,7 +1135,8 @@ public class BubbleController { if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) { continue; } - if (reason == DISMISS_NOTIF_CANCEL) { + if (reason == DISMISS_NOTIF_CANCEL + || reason == DISMISS_SHORTCUT_REMOVED) { bubblesToBeRemovedFromRepository.add(bubble); } if (!mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { @@ -1366,8 +1380,9 @@ public class BubbleController { private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener { @Override public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { + mBubblePositioner.setImeVisible(imeVisible, imeHeight); if (mStackView != null) { - mStackView.onImeVisibilityChanged(imeVisible, imeHeight); + mStackView.animateForIme(imeVisible); } } } @@ -1566,13 +1581,6 @@ public class BubbleController { } @Override - public void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback) { - mMainExecutor.execute(() -> { - BubbleController.this.setBubbleScrim(view, callback); - }); - } - - @Override public void setExpandListener(BubbleExpandListener listener) { mMainExecutor.execute(() -> { BubbleController.this.setExpandListener(listener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index d73ce6951e6d..cd635c10fd8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -323,11 +323,12 @@ public class BubbleData { } mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); - suppressFlyout |= !bubble.isVisuallyInterruptive(); + suppressFlyout |= !bubble.isTextChanged(); if (prevBubble == null) { // Create a new bubble bubble.setSuppressFlyout(suppressFlyout); + bubble.markUpdatedAt(mTimeSource.currentTimeMillis()); doAdd(bubble); trim(); } else { @@ -558,6 +559,8 @@ public class BubbleData { } Bubble bubbleToRemove = mBubbles.get(indexToRemove); bubbleToRemove.stopInflation(); + overflowBubble(reason, bubbleToRemove); + if (mBubbles.size() == 1) { if (hasOverflowBubbles() && (mPositioner.showingInTaskbar() || isExpanded())) { // No more active bubbles but we have stuff in the overflow -- select that view @@ -581,8 +584,6 @@ public class BubbleData { mStateChange.orderChanged |= repackAll(); } - overflowBubble(reason, bubbleToRemove); - // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. if (Objects.equals(mSelectedBubble, bubbleToRemove)) { // Move selection to the new bubble at the same position. @@ -699,10 +700,9 @@ public class BubbleData { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "setSelectedBubbleInternal: " + bubble); } - if (!mShowingOverflow && Objects.equals(bubble, mSelectedBubble)) { + if (Objects.equals(bubble, mSelectedBubble)) { return; } - // Otherwise, if we are showing the overflow menu, return to the previously selected bubble. boolean isOverflow = bubble != null && BubbleOverflow.KEY.equals(bubble.getKey()); if (bubble != null && !mBubbles.contains(bubble) @@ -771,6 +771,10 @@ public class BubbleData { Log.e(TAG, "Attempt to expand stack without selected bubble!"); return; } + if (mSelectedBubble.getKey().equals(mOverflow.getKey()) && !mBubbles.isEmpty()) { + // Show previously selected bubble instead of overflow menu when expanding. + setSelectedBubbleInternal(mBubbles.get(0)); + } if (mSelectedBubble instanceof Bubble) { ((Bubble) mSelectedBubble).markAsAccessedAt(mTimeSource.currentTimeMillis()); } @@ -779,16 +783,6 @@ public class BubbleData { // Apply ordering and grouping rules from expanded -> collapsed, then save // the result. mStateChange.orderChanged |= repackAll(); - // Save the state which should be returned to when expanded (with no other changes) - - if (mShowingOverflow) { - // Show previously selected bubble instead of overflow menu on next expansion. - if (!mSelectedBubble.getKey().equals(mOverflow.getKey())) { - setSelectedBubbleInternal(mSelectedBubble); - } else { - setSelectedBubbleInternal(mBubbles.get(0)); - } - } if (mBubbles.indexOf(mSelectedBubble) > 0) { // Move the selected bubble to the top while collapsed. int index = mBubbles.indexOf(mSelectedBubble); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 9687ec6a8168..a87aad4261a6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -25,6 +25,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT; import android.annotation.NonNull; import android.annotation.SuppressLint; @@ -60,7 +61,6 @@ import android.widget.LinearLayout; import androidx.annotation.Nullable; import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import com.android.wm.shell.TaskView; import com.android.wm.shell.common.AlphaOptimizedButton; @@ -77,7 +77,6 @@ public class BubbleExpandedView extends LinearLayout { // The triangle pointing to the expanded view private View mPointerView; - private int mPointerMargin; @Nullable private int[] mExpandedViewContainerLocation; private AlphaOptimizedButton mManageButton; @@ -102,9 +101,6 @@ public class BubbleExpandedView extends LinearLayout { */ private boolean mIsAlphaAnimating = false; - private int mMinHeight; - private int mOverflowHeight; - private int mManageButtonHeight; private int mPointerWidth; private int mPointerHeight; private float mPointerRadius; @@ -232,7 +228,7 @@ public class BubbleExpandedView extends LinearLayout { @Override public void onBackPressedOnTaskRoot(int taskId) { if (mTaskId == taskId && mStackView.isExpanded()) { - mController.collapseStack(); + mStackView.onBackPressed(); } } }; @@ -338,7 +334,8 @@ public class BubbleExpandedView extends LinearLayout { bringChildToFront(mOverflowView); mManageButton.setVisibility(GONE); } else { - mTaskView = new TaskView(mContext, mController.getTaskOrganizer()); + mTaskView = new TaskView(mContext, mController.getTaskOrganizer(), + mController.getSyncTransactionQueue()); mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); mExpandedViewContainer.addView(mTaskView); bringChildToFront(mTaskView); @@ -347,12 +344,8 @@ public class BubbleExpandedView extends LinearLayout { void updateDimensions() { Resources res = getResources(); - mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); - mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); - updateFontSize(); - mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mPointerRadius = getResources().getDimensionPixelSize(R.dimen.bubble_pointer_radius); @@ -368,7 +361,6 @@ public class BubbleExpandedView extends LinearLayout { updatePointerView(); } - mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_height); if (mManageButton != null) { int visibility = mManageButton.getVisibility(); removeView(mManageButton); @@ -406,6 +398,7 @@ public class BubbleExpandedView extends LinearLayout { updatePointerView(); } + /** Updates the size and visuals of the pointer. **/ private void updatePointerView() { LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams(); if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) { @@ -532,9 +525,8 @@ public class BubbleExpandedView extends LinearLayout { if (mTaskView != null) { mTaskView.setAlpha(alpha); } - if (mManageButton != null && mManageButton.getVisibility() == View.VISIBLE) { - mManageButton.setAlpha(alpha); - } + mPointerView.setAlpha(alpha); + setAlpha(alpha); } /** @@ -553,6 +545,7 @@ public class BubbleExpandedView extends LinearLayout { mIsContentVisible = visibility; if (mTaskView != null && !mIsAlphaAnimating) { mTaskView.setAlpha(visibility ? 1f : 0f); + mPointerView.setAlpha(visibility ? 1f : 0f); } } @@ -632,12 +625,11 @@ public class BubbleExpandedView extends LinearLayout { } if ((mBubble != null && mTaskView != null) || mIsOverflow) { - float desiredHeight = mIsOverflow - ? mPositioner.isLargeScreen() ? getMaxExpandedHeight() : mOverflowHeight - : mBubble.getDesiredHeight(mContext); - desiredHeight = Math.max(desiredHeight, mMinHeight); - float height = Math.min(desiredHeight, getMaxExpandedHeight()); - height = Math.max(height, mMinHeight); + float desiredHeight = mPositioner.getExpandedViewHeight(mBubble); + int maxHeight = mPositioner.getMaxExpandedViewHeight(mIsOverflow); + float height = desiredHeight == MAX_HEIGHT + ? maxHeight + : Math.min(desiredHeight, maxHeight); FrameLayout.LayoutParams lp = mIsOverflow ? (FrameLayout.LayoutParams) mOverflowView.getLayoutParams() : (FrameLayout.LayoutParams) mTaskView.getLayoutParams(); @@ -661,23 +653,6 @@ public class BubbleExpandedView extends LinearLayout { } } - private int getMaxExpandedHeight() { - int expandedContainerY = mExpandedViewContainerLocation != null - // Remove top insets back here because availableRect.height would account for that - ? mExpandedViewContainerLocation[1] - mPositioner.getInsets().top - : 0; - int settingsHeight = mIsOverflow ? 0 : mManageButtonHeight; - int pointerHeight = mPositioner.showBubblesVertically() - ? mPointerWidth - : (int) (mPointerHeight - mPointerOverlap + mPointerMargin); - return mPositioner.getAvailableRect().height() - - expandedContainerY - - getPaddingTop() - - getPaddingBottom() - - settingsHeight - - pointerHeight; - } - /** * Update appearance of the expanded view being displayed. * @@ -715,28 +690,29 @@ public class BubbleExpandedView extends LinearLayout { * the bubble if showing vertically. * @param onLeft whether the stack was on the left side of the screen when expanded. */ - public void setPointerPosition(float bubblePosition, boolean onLeft) { + public void setPointerPosition(float bubblePosition, boolean onLeft, boolean animate) { // Pointer gets drawn in the padding final boolean showVertically = mPositioner.showBubblesVertically(); final float paddingLeft = (showVertically && onLeft) ? mPointerHeight - mPointerOverlap : 0; final float paddingRight = (showVertically && !onLeft) - ? mPointerHeight - mPointerOverlap : 0; - final float paddingTop = showVertically ? 0 + ? mPointerHeight - mPointerOverlap + : 0; + final float paddingTop = showVertically + ? 0 : mPointerHeight - mPointerOverlap; setPadding((int) paddingLeft, (int) paddingTop, (int) paddingRight, 0); - final float expandedViewY = mPositioner.getExpandedViewY(); - // TODO: I don't understand why it works but it does - why normalized in portrait - // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? - final float normalizedSize = IconNormalizer.getNormalizedCircleSize( - mPositioner.getBubbleSize()); - final float bubbleCenter = showVertically - ? bubblePosition + (mPositioner.getBubbleSize() / 2f) - expandedViewY - : bubblePosition + (normalizedSize / 2f) - mPointerWidth; + // Subtract the expandedViewY here because the pointer is placed within the expandedView. + float pointerPosition = mPositioner.getPointerPosition(bubblePosition); + final float bubbleCenter = mPositioner.showBubblesVertically() + ? pointerPosition - mPositioner.getExpandedViewY(mBubble, bubblePosition) + : pointerPosition; // Post because we need the width of the view post(() -> { + mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; + updatePointerView(); float pointerY; float pointerX; if (showVertically) { @@ -748,11 +724,13 @@ public class BubbleExpandedView extends LinearLayout { pointerY = mPointerOverlap; pointerX = bubbleCenter - (mPointerWidth / 2f); } - mPointerView.setTranslationY(pointerY); - mPointerView.setTranslationX(pointerX); - mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer; - updatePointerView(); - mPointerView.setVisibility(VISIBLE); + if (animate) { + mPointerView.animate().translationX(pointerX).translationY(pointerY).start(); + } else { + mPointerView.setTranslationY(pointerY); + mPointerView.setTranslationX(pointerX); + mPointerView.setVisibility(VISIBLE); + } }); } @@ -764,6 +742,10 @@ public class BubbleExpandedView extends LinearLayout { mManageButton.getBoundsOnScreen(rect); } + public int getManageButtonMargin() { + return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart(); + } + /** * Cleans up anything related to the task and {@code TaskView}. If this view should be reused * after this method is called, then diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java index 35a4f33ecf72..f878a46d26f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java @@ -56,9 +56,6 @@ import com.android.wm.shell.common.TriangleShape; * transform into the 'new' dot, which is used during flyout dismiss animations/gestures. */ public class BubbleFlyoutView extends FrameLayout { - /** Max width of the flyout, in terms of percent of the screen width. */ - private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; - /** Translation Y of fade animation. */ private static final float FLYOUT_FADE_Y = 40f; @@ -68,6 +65,8 @@ public class BubbleFlyoutView extends FrameLayout { // Whether the flyout view should show a pointer to the bubble. private static final boolean SHOW_POINTER = false; + private BubblePositioner mPositioner; + private final int mFlyoutPadding; private final int mFlyoutSpaceFromBubble; private final int mPointerSize; @@ -156,10 +155,11 @@ public class BubbleFlyoutView extends FrameLayout { /** Callback to run when the flyout is hidden. */ @Nullable private Runnable mOnHide; - public BubbleFlyoutView(Context context) { + public BubbleFlyoutView(Context context, BubblePositioner positioner) { super(context); - LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); + mPositioner = positioner; + LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true); mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container); mSenderText = findViewById(R.id.bubble_flyout_name); mSenderAvatar = findViewById(R.id.bubble_flyout_avatar); @@ -230,11 +230,12 @@ public class BubbleFlyoutView extends FrameLayout { /* * Fade animation for consecutive flyouts. */ - void animateUpdate(Bubble.FlyoutMessage flyoutMessage, float parentWidth, PointF stackPos, - boolean hideDot, Runnable onHide) { + void animateUpdate(Bubble.FlyoutMessage flyoutMessage, PointF stackPos, + boolean hideDot, float[] dotCenter, Runnable onHide) { mOnHide = onHide; + mDotCenter = dotCenter; final Runnable afterFadeOut = () -> { - updateFlyoutMessage(flyoutMessage, parentWidth); + updateFlyoutMessage(flyoutMessage); // Wait for TextViews to layout with updated height. post(() -> { fade(true /* in */, stackPos, hideDot, () -> {} /* after */); @@ -266,7 +267,7 @@ public class BubbleFlyoutView extends FrameLayout { .withEndAction(afterFade); } - private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage, float parentWidth) { + private void updateFlyoutMessage(Bubble.FlyoutMessage flyoutMessage) { final Drawable senderAvatar = flyoutMessage.senderAvatar; if (senderAvatar != null && flyoutMessage.isGroupChat) { mSenderAvatar.setVisibility(VISIBLE); @@ -278,8 +279,7 @@ public class BubbleFlyoutView extends FrameLayout { mSenderText.setTranslationX(0); } - final int maxTextViewWidth = - (int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2; + final int maxTextViewWidth = (int) mPositioner.getMaxFlyoutSize() - mFlyoutPadding * 2; // Name visibility if (!TextUtils.isEmpty(flyoutMessage.senderName)) { @@ -328,22 +328,20 @@ public class BubbleFlyoutView extends FrameLayout { void setupFlyoutStartingAsDot( Bubble.FlyoutMessage flyoutMessage, PointF stackPos, - float parentWidth, boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete, @Nullable Runnable onHide, float[] dotCenter, - boolean hideDot, - BubblePositioner positioner) { + boolean hideDot) { - mBubbleSize = positioner.getBubbleSize(); + mBubbleSize = mPositioner.getBubbleSize(); mOriginalDotSize = SIZE_PERCENTAGE * mBubbleSize; mNewDotRadius = (DOT_SCALE * mOriginalDotSize) / 2f; mNewDotSize = mNewDotRadius * 2f; - updateFlyoutMessage(flyoutMessage, parentWidth); + updateFlyoutMessage(flyoutMessage); mArrowPointingLeft = arrowPointingLeft; mDotColor = dotColor; 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 705a12a5e65b..0c3a6b2dbd84 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 @@ -154,10 +154,6 @@ class BubbleOverflow( return dotPath } - override fun setExpandedContentAlpha(alpha: Float) { - expandedView?.alpha = alpha - } - override fun setTaskViewVisibility(visible: Boolean) { // Overflow does not have a TaskView. } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java index ede42285d9cd..5e9d97f23c57 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java @@ -142,7 +142,7 @@ public class BubbleOverflowContainerView extends LinearLayout { super.onAttachedToWindow(); if (mController != null) { // For the overflow to get key events (e.g. back press) we need to adjust the flags - mController.updateWindowFlagsForOverflow(true); + mController.updateWindowFlagsForBackpress(true); } setOnKeyListener(mKeyListener); } @@ -151,7 +151,7 @@ public class BubbleOverflowContainerView extends LinearLayout { protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mController != null) { - mController.updateWindowFlagsForOverflow(false); + mController.updateWindowFlagsForBackpress(false); } setOnKeyListener(null); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index c600f56ba0c5..127d5a8a9966 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -34,6 +34,7 @@ import android.view.WindowMetrics; import androidx.annotation.VisibleForTesting; +import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import java.lang.annotation.Retention; @@ -58,29 +59,48 @@ public class BubblePositioner { /** When the bubbles are collapsed in a stack only some of them are shown, this is how many. **/ public static final int NUM_VISIBLE_WHEN_RESTING = 2; + /** Indicates a bubble's height should be the maximum available space. **/ + public static final int MAX_HEIGHT = -1; + /** The max percent of screen width to use for the flyout on large screens. */ + public static final float FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN = 0.3f; + /** The max percent of screen width to use for the flyout on phone. */ + public static final float FLYOUT_MAX_WIDTH_PERCENT = 0.6f; + /** The percent of screen width that should be used for the expanded view on a large screen. **/ + public static final float EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT = 0.72f; private Context mContext; private WindowManager mWindowManager; - private Rect mPositionRect; + private Rect mScreenRect; private @Surface.Rotation int mRotation = Surface.ROTATION_0; private Insets mInsets; + private boolean mImeVisible; + private int mImeHeight; + private boolean mIsLargeScreen; + + private Rect mPositionRect; private int mDefaultMaxBubbles; private int mMaxBubbles; - private int mBubbleSize; - private int mBubbleBadgeSize; private int mSpacingBetweenBubbles; + + private int mExpandedViewMinHeight; private int mExpandedViewLargeScreenWidth; + private int mExpandedViewLargeScreenInset; + + private int mOverflowWidth; private int mExpandedViewPadding; private int mPointerMargin; - private float mPointerWidth; - private float mPointerHeight; + private int mPointerWidth; + private int mPointerHeight; + private int mPointerOverlap; + private int mManageButtonHeight; + private int mOverflowHeight; + private int mMinimumFlyoutWidthLargeScreen; private PointF mPinLocation; private PointF mRestingStackPosition; private int[] mPaddings = new int[4]; - private boolean mIsLargeScreen; private boolean mShowingInTaskbar; private @TaskbarPosition int mTaskbarPosition = TASKBAR_POSITION_NONE; private int mTaskbarIconSize; @@ -143,6 +163,7 @@ public class BubblePositioner { mRotation = rotation; mInsets = insets; + mScreenRect = new Rect(bounds); mPositionRect = new Rect(bounds); mPositionRect.left += mInsets.left; mPositionRect.top += mInsets.top; @@ -151,16 +172,27 @@ public class BubblePositioner { Resources res = mContext.getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); - mBubbleBadgeSize = res.getDimensionPixelSize(R.dimen.bubble_badge_size); mSpacingBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); mDefaultMaxBubbles = res.getInteger(R.integer.bubbles_max_rendered); - - mExpandedViewLargeScreenWidth = res.getDimensionPixelSize( - R.dimen.bubble_expanded_view_tablet_width); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); + mExpandedViewLargeScreenWidth = (int) (bounds.width() + * EXPANDED_VIEW_LARGE_SCREEN_WIDTH_PERCENT); + mExpandedViewLargeScreenInset = mIsLargeScreen + ? (bounds.width() - mExpandedViewLargeScreenWidth) / 2 + : mExpandedViewPadding; + mOverflowWidth = mIsLargeScreen + ? mExpandedViewLargeScreenWidth + : res.getDimensionPixelSize( + R.dimen.bubble_expanded_view_phone_landscape_overflow_width); mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); + mPointerOverlap = res.getDimensionPixelSize(R.dimen.bubble_pointer_overlap); + mManageButtonHeight = res.getDimensionPixelSize(R.dimen.bubble_manage_button_total_height); + mExpandedViewMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); + mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); + mMinimumFlyoutWidthLargeScreen = res.getDimensionPixelSize( + R.dimen.bubbles_flyout_min_width_large_screen); mMaxBubbles = calculateMaxBubbles(); @@ -225,6 +257,13 @@ public class BubblePositioner { } /** + * @return a rect of the screen size. + */ + public Rect getScreenRect() { + return mScreenRect; + } + + /** * @return the relevant insets (status bar, nav bar, cutouts). If taskbar is showing, its * inset is not included here. */ @@ -265,48 +304,287 @@ public class BubblePositioner { return mMaxBubbles; } + /** The height for the IME if it's visible. **/ + public int getImeHeight() { + return mImeVisible ? mImeHeight : 0; + } + + /** Sets whether the IME is visible. **/ + public void setImeVisible(boolean visible, int height) { + mImeVisible = visible; + mImeHeight = height; + } + /** - * Calculates the left & right padding for the bubble expanded view. + * Calculates the padding for the bubble expanded view. * - * On larger screens the width of the expanded view is restricted via this padding. - * On landscape the bubble overflow expanded view is also restricted via this padding. + * Some specifics: + * On large screens the width of the expanded view is restricted via this padding. + * On phone landscape the bubble overflow expanded view is also restricted via this padding. + * On large screens & landscape no top padding is set, the top position is set via translation. + * On phone portrait top padding is set as the space between the tip of the pointer and the + * bubble. + * When the overflow is shown it doesn't have the manage button to pad out the bottom so + * padding is added. */ - public int[] getExpandedViewPadding(boolean onLeft, boolean isOverflow) { - int leftPadding = mInsets.left + mExpandedViewPadding; - int rightPadding = mInsets.right + mExpandedViewPadding; - final boolean isLargeOrOverflow = mIsLargeScreen || isOverflow; - if (showBubblesVertically()) { - if (!onLeft) { - rightPadding += mBubbleSize - mPointerHeight; - leftPadding += isLargeOrOverflow - ? (mPositionRect.width() - rightPadding - mExpandedViewLargeScreenWidth) - : 0; - } else { - leftPadding += mBubbleSize - mPointerHeight; - rightPadding += isLargeOrOverflow - ? (mPositionRect.width() - leftPadding - mExpandedViewLargeScreenWidth) - : 0; + public int[] getExpandedViewContainerPadding(boolean onLeft, boolean isOverflow) { + final int pointerTotalHeight = mPointerHeight - mPointerOverlap; + if (mIsLargeScreen) { + // [left, top, right, bottom] + mPaddings[0] = onLeft + ? mExpandedViewLargeScreenInset - pointerTotalHeight + : mExpandedViewLargeScreenInset; + mPaddings[1] = 0; + mPaddings[2] = onLeft + ? mExpandedViewLargeScreenInset + : mExpandedViewLargeScreenInset - pointerTotalHeight; + // Overflow doesn't show manage button / get padding from it so add padding here for it + mPaddings[3] = isOverflow ? mExpandedViewPadding : 0; + return mPaddings; + } else { + int leftPadding = mInsets.left + mExpandedViewPadding; + int rightPadding = mInsets.right + mExpandedViewPadding; + final float expandedViewWidth = isOverflow + ? mOverflowWidth + : mExpandedViewLargeScreenWidth; + if (showBubblesVertically()) { + if (!onLeft) { + rightPadding += mBubbleSize - pointerTotalHeight; + leftPadding += isOverflow + ? (mPositionRect.width() - rightPadding - expandedViewWidth) + : 0; + } else { + leftPadding += mBubbleSize - pointerTotalHeight; + rightPadding += isOverflow + ? (mPositionRect.width() - leftPadding - expandedViewWidth) + : 0; + } } + // [left, top, right, bottom] + mPaddings[0] = leftPadding; + mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin; + mPaddings[2] = rightPadding; + mPaddings[3] = 0; + return mPaddings; } - // [left, top, right, bottom] - mPaddings[0] = leftPadding; - mPaddings[1] = showBubblesVertically() ? 0 : mPointerMargin; - mPaddings[2] = rightPadding; - mPaddings[3] = 0; - return mPaddings; } - /** Calculates the y position of the expanded view when it is expanded. */ - public float getExpandedViewY() { + /** Gets the y position of the expanded view if it was top-aligned. */ + public float getExpandedViewYTopAligned() { final int top = getAvailableRect().top; if (showBubblesVertically()) { - return top - mPointerWidth; + return top - mPointerWidth + mExpandedViewPadding; } else { return top + mBubbleSize + mPointerMargin; } } /** + * Calculate the maximum height the expanded view can be depending on where it's placed on + * the screen and the size of the elements around it (e.g. padding, pointer, manage button). + */ + public int getMaxExpandedViewHeight(boolean isOverflow) { + // Subtract top insets because availableRect.height would account for that + int expandedContainerY = (int) getExpandedViewYTopAligned() - getInsets().top; + int paddingTop = showBubblesVertically() + ? 0 + : mPointerHeight; + // Subtract pointer size because it's laid out in LinearLayout with the expanded view. + int pointerSize = showBubblesVertically() + ? mPointerWidth + : (mPointerHeight + mPointerMargin); + int bottomPadding = isOverflow ? mExpandedViewPadding : mManageButtonHeight; + return getAvailableRect().height() + - expandedContainerY + - paddingTop + - pointerSize + - bottomPadding; + } + + /** + * Determines the height for the bubble, ensuring a minimum height. If the height should be as + * big as available, returns {@link #MAX_HEIGHT}. + */ + public float getExpandedViewHeight(BubbleViewProvider bubble) { + boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); + if (isOverflow && showBubblesVertically() && !mIsLargeScreen) { + // overflow in landscape on phone is max + return MAX_HEIGHT; + } + float desiredHeight = isOverflow + ? mOverflowHeight + : ((Bubble) bubble).getDesiredHeight(mContext); + desiredHeight = Math.max(desiredHeight, mExpandedViewMinHeight); + if (desiredHeight > getMaxExpandedViewHeight(isOverflow)) { + return MAX_HEIGHT; + } + return desiredHeight; + } + + /** + * Gets the y position for the expanded view. This is the position on screen of the top + * horizontal line of the expanded view. + * + * @param bubble the bubble being positioned. + * @param bubblePosition the x position of the bubble if showing on top, the y position of the + * bubble if showing vertically. + * @return the y position for the expanded view. + */ + public float getExpandedViewY(BubbleViewProvider bubble, float bubblePosition) { + boolean isOverflow = bubble == null || BubbleOverflow.KEY.equals(bubble.getKey()); + float expandedViewHeight = getExpandedViewHeight(bubble); + float topAlignment = getExpandedViewYTopAligned(); + if (!showBubblesVertically() || expandedViewHeight == MAX_HEIGHT) { + // Top-align when bubbles are shown at the top or are max size. + return topAlignment; + } + // If we're here, we're showing vertically & developer has made height less than maximum. + int manageButtonHeight = isOverflow ? mExpandedViewPadding : mManageButtonHeight; + float pointerPosition = getPointerPosition(bubblePosition); + float bottomIfCentered = pointerPosition + (expandedViewHeight / 2) + manageButtonHeight; + float topIfCentered = pointerPosition - (expandedViewHeight / 2); + if (topIfCentered > mPositionRect.top && mPositionRect.bottom > bottomIfCentered) { + // Center it + return pointerPosition - mPointerWidth - (expandedViewHeight / 2f); + } else if (topIfCentered <= mPositionRect.top) { + // Top align + return topAlignment; + } else { + // Bottom align + return mPositionRect.bottom - manageButtonHeight - expandedViewHeight - mPointerWidth; + } + } + + /** + * The position the pointer points to, the center of the bubble. + * + * @param bubblePosition the x position of the bubble if showing on top, the y position of the + * bubble if showing vertically. + * @return the position the tip of the pointer points to. The x position if showing on top, the + * y position if showing vertically. + */ + public float getPointerPosition(float bubblePosition) { + // TODO: I don't understand why it works but it does - why normalized in portrait + // & not in landscape? Am I missing ~2dp in the portrait expandedViewY calculation? + final float normalizedSize = IconNormalizer.getNormalizedCircleSize( + getBubbleSize()); + return showBubblesVertically() + ? bubblePosition + (getBubbleSize() / 2f) + : bubblePosition + (normalizedSize / 2f) - mPointerWidth; + } + + private int getExpandedStackSize(int numberOfBubbles) { + return (numberOfBubbles * mBubbleSize) + + ((numberOfBubbles - 1) * mSpacingBetweenBubbles); + } + + /** + * Returns the position of the bubble on-screen when the stack is expanded. + * + * @param index the index of the bubble in the stack. + * @param state state information about the stack to help with calculations. + * @return the position of the bubble on-screen when the stack is expanded. + */ + public PointF getExpandedBubbleXY(int index, BubbleStackView.StackViewState state) { + final float positionInRow = index * (mBubbleSize + mSpacingBetweenBubbles); + final float expandedStackSize = getExpandedStackSize(state.numberOfBubbles); + final float centerPosition = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + // alignment - centered on the edge + final float rowStart = centerPosition - (expandedStackSize / 2f); + float x; + float y; + if (showBubblesVertically()) { + y = rowStart + positionInRow; + int left = mIsLargeScreen + ? mExpandedViewLargeScreenInset - mExpandedViewPadding - mBubbleSize + : mPositionRect.left; + int right = mIsLargeScreen + ? mPositionRect.right - mExpandedViewLargeScreenInset + mExpandedViewPadding + : mPositionRect.right - mBubbleSize; + x = state.onLeft + ? left + : right; + } else { + y = mPositionRect.top + mExpandedViewPadding; + x = rowStart + positionInRow; + } + + if (showBubblesVertically() && mImeVisible) { + return new PointF(x, getExpandedBubbleYForIme(index, state.numberOfBubbles)); + } + return new PointF(x, y); + } + + /** + * Returns the position of the bubble on-screen when the stack is expanded and the IME + * is showing. + * + * @param index the index of the bubble in the stack. + * @param numberOfBubbles the total number of bubbles in the stack. + * @return y position of the bubble on-screen when the stack is expanded. + */ + private float getExpandedBubbleYForIme(int index, int numberOfBubbles) { + final float top = getAvailableRect().top + mExpandedViewPadding; + if (!showBubblesVertically()) { + // Showing horizontally: align to top + return top; + } + + // Showing vertically: might need to translate the bubbles above the IME. + // Subtract spacing here to provide a margin between top of IME and bottom of bubble row. + final float bottomInset = getImeHeight() + mInsets.bottom - (mSpacingBetweenBubbles * 2); + final float expandedStackSize = getExpandedStackSize(numberOfBubbles); + final float centerPosition = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + final float rowBottom = centerPosition + (expandedStackSize / 2f); + final float rowTop = centerPosition - (expandedStackSize / 2f); + float rowTopForIme = rowTop; + if (rowBottom > bottomInset) { + // We overlap with IME, must shift the bubbles + float translationY = rowBottom - bottomInset; + rowTopForIme = Math.max(rowTop - translationY, top); + if (rowTop - translationY < top) { + // Even if we shift the bubbles, they will still overlap with the IME. + // Hide the overflow for a lil more space: + final float expandedStackSizeNoO = getExpandedStackSize(numberOfBubbles - 1); + final float centerPositionNoO = showBubblesVertically() + ? mPositionRect.centerY() + : mPositionRect.centerX(); + final float rowBottomNoO = centerPositionNoO + (expandedStackSizeNoO / 2f); + final float rowTopNoO = centerPositionNoO - (expandedStackSizeNoO / 2f); + translationY = rowBottomNoO - bottomInset; + rowTopForIme = rowTopNoO - translationY; + } + } + return rowTopForIme + (index * (mBubbleSize + mSpacingBetweenBubbles)); + } + + /** + * @return the width of the bubble flyout (message originating from the bubble). + */ + public float getMaxFlyoutSize() { + if (isLargeScreen()) { + return Math.max(mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT_LARGE_SCREEN, + mMinimumFlyoutWidthLargeScreen); + } + return mScreenRect.width() * FLYOUT_MAX_WIDTH_PERCENT; + } + + /** + * @return whether the stack is considered on the left side of the screen. + */ + public boolean isStackOnLeft(PointF currentStackPosition) { + if (currentStackPosition == null) { + currentStackPosition = getRestingPosition(); + } + final int stackCenter = (int) currentStackPosition.x + mBubbleSize / 2; + return stackCenter < mScreenRect.width() / 2; + } + + /** * Sets the stack's most recent position along the edge of the screen. This is saved when the * last bubble is removed, so that the stack can be restored in its previous position. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index ac97c8f80617..14433c233273 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -19,6 +19,8 @@ package com.android.wm.shell.bubbles; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -26,18 +28,21 @@ import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RES import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.Outline; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.provider.Settings; import android.util.Log; @@ -106,14 +111,8 @@ public class BubbleStackView extends FrameLayout */ private static final float FLYOUT_OVERSCROLL_ATTENUATION_FACTOR = 8f; - /** Duration of the flyout alpha animations. */ - private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100; - private static final int FADE_IN_DURATION = 320; - /** Percent to darken the bubbles when they're in the dismiss target. */ - private static final float DARKEN_PERCENT = 0.3f; - /** How long to wait, in milliseconds, before hiding the flyout. */ @VisibleForTesting static final int FLYOUT_HIDE_AFTER = 5000; @@ -122,6 +121,8 @@ public class BubbleStackView extends FrameLayout private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; + private static final float SCRIM_ALPHA = 0.6f; + /** * How long to wait to animate the stack temporarily invisible after a drag/flyout hide * animation ends, if we are in fact temporarily invisible. @@ -188,6 +189,7 @@ public class BubbleStackView extends FrameLayout }; private final BubbleController mBubbleController; private final BubbleData mBubbleData; + private StackViewState mStackViewState = new StackViewState(); private final ValueAnimator mDismissBubbleAnimator; @@ -195,7 +197,8 @@ public class BubbleStackView extends FrameLayout private StackAnimationController mStackAnimationController; private ExpandedAnimationController mExpandedAnimationController; - private View mTaskbarScrim; + private View mScrim; + private View mManageMenuScrim; private FrameLayout mExpandedViewContainer; /** Matrix used to scale the expanded view container with a given pivot point. */ @@ -245,7 +248,6 @@ public class BubbleStackView extends FrameLayout private int mBubbleTouchPadding; private int mExpandedViewPadding; private int mCornerRadius; - private int mImeOffset; @Nullable private BubbleViewProvider mExpandedBubble; private boolean mIsExpanded; @@ -528,9 +530,10 @@ public class BubbleStackView extends FrameLayout // Otherwise, we either tapped the stack (which means we're collapsed // and should expand) or the currently selected bubble (we're expanded // and should collapse). - if (!maybeShowStackEdu()) { + if (!maybeShowStackEdu() && !mShowedUserEducationInTouchListenerActive) { mBubbleData.setExpanded(!mBubbleData.isExpanded()); } + mShowedUserEducationInTouchListenerActive = false; } } }; @@ -548,6 +551,14 @@ public class BubbleStackView extends FrameLayout return true; } + mShowedUserEducationInTouchListenerActive = false; + if (maybeShowStackEdu()) { + mShowedUserEducationInTouchListenerActive = true; + return true; + } else if (isStackEduShowing()) { + mStackEduView.hide(false /* fromExpansion */); + } + // If the manage menu is visible, just hide it. if (mShowingManage) { showManageMenu(false /* show */); @@ -555,7 +566,7 @@ public class BubbleStackView extends FrameLayout if (mBubbleData.isExpanded()) { if (mManageEduView != null) { - mManageEduView.hide(false /* show */); + mManageEduView.hide(); } // If we're expanded, tell the animation controller to prepare to drag this bubble, @@ -606,7 +617,8 @@ public class BubbleStackView extends FrameLayout // If we're expanding or collapsing, ignore all touch events. if (mIsExpansionAnimating // Also ignore events if we shouldn't be draggable. - || (mPositioner.showingInTaskbar() && !mIsExpanded)) { + || (mPositioner.showingInTaskbar() && !mIsExpanded) + || mShowedUserEducationInTouchListenerActive) { return; } @@ -627,7 +639,7 @@ public class BubbleStackView extends FrameLayout mExpandedAnimationController.dragBubbleOut( v, viewInitialX + dx, viewInitialY + dy); } else { - if (mStackEduView != null) { + if (isStackEduShowing()) { mStackEduView.hide(false /* fromExpansion */); } mStackAnimationController.moveStackFromTouch( @@ -645,6 +657,10 @@ public class BubbleStackView extends FrameLayout || (mPositioner.showingInTaskbar() && !mIsExpanded)) { return; } + if (mShowedUserEducationInTouchListenerActive) { + mShowedUserEducationInTouchListenerActive = false; + return; + } // First, see if the magnetized object consumes the event - if so, the bubble was // released in the target or flung out of it, and we should ignore the event. @@ -737,6 +753,7 @@ public class BubbleStackView extends FrameLayout private ImageView mManageSettingsIcon; private TextView mManageSettingsText; private boolean mShowingManage = false; + private boolean mShowedUserEducationInTouchListenerActive = false; private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); private BubblePositioner mPositioner; @@ -756,7 +773,6 @@ public class BubbleStackView extends FrameLayout mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding); - mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset); mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); @@ -777,8 +793,8 @@ public class BubbleStackView extends FrameLayout floatingContentCoordinator, this::getBubbleCount, onBubbleAnimatedOut, this::animateShadows /* onStackAnimationFinished */, mPositioner); - mExpandedAnimationController = new ExpandedAnimationController( - mPositioner, mExpandedViewPadding, onBubbleAnimatedOut); + mExpandedAnimationController = new ExpandedAnimationController(mPositioner, + onBubbleAnimatedOut, this); mSurfaceSynchronizer = synchronizer != null ? synchronizer : DEFAULT_SURFACE_SYNCHRONIZER; // Force LTR by default since most of the Bubbles UI is positioned manually by the user, or @@ -793,8 +809,6 @@ public class BubbleStackView extends FrameLayout mBubbleContainer.setClipChildren(false); addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); - updateUserEdu(); - mExpandedViewContainer = new FrameLayout(context); mExpandedViewContainer.setElevation(elevation); mExpandedViewContainer.setClipChildren(false); @@ -858,11 +872,20 @@ public class BubbleStackView extends FrameLayout mBubbleData.setExpanded(true); }); - mTaskbarScrim = new View(getContext()); - mTaskbarScrim.setBackgroundColor(Color.BLACK); - addView(mTaskbarScrim); - mTaskbarScrim.setAlpha(0f); - mTaskbarScrim.setVisibility(GONE); + mScrim = new View(getContext()); + mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + addView(mScrim); + mScrim.setAlpha(0f); + + mManageMenuScrim = new View(getContext()); + mManageMenuScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + addView(mManageMenuScrim, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mManageMenuScrim.setAlpha(0f); + mManageMenuScrim.setVisibility(INVISIBLE); mOrientationChangedListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { @@ -882,12 +905,15 @@ public class BubbleStackView extends FrameLayout // Re-draw bubble row and pointer for new orientation. beforeExpandedViewAnimation(); updateOverflowVisibility(); - updatePointerPosition(); + updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { afterExpandedViewAnimation(); + showManageMenu(mShowingManage); } /* after */); + final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, + getBubbleIndex(mExpandedBubble)); mExpandedViewContainer.setTranslationX(0f); - mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); + mExpandedViewContainer.setTranslationY(translationY); mExpandedViewContainer.setAlpha(1f); } removeOnLayoutChangeListener(mOrientationChangedListener); @@ -917,10 +943,14 @@ public class BubbleStackView extends FrameLayout setOnClickListener(view -> { if (mShowingManage) { showManageMenu(false /* show */); - } else if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) { - mStackEduView.hide(false); + } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); + } else if (isStackEduShowing()) { + mStackEduView.hide(false /* isExpanding */); } else if (mBubbleData.isExpanded()) { mBubbleData.setExpanded(false); + } else { + maybeShowStackEdu(); } }); @@ -955,8 +985,9 @@ public class BubbleStackView extends FrameLayout } }); mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { - if (mExpandedBubble != null) { - mExpandedBubble.setExpandedContentAlpha((float) valueAnimator.getAnimatedValue()); + if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { + mExpandedBubble.getExpandedView().setTaskViewAlpha( + (float) valueAnimator.getAnimatedValue()); } }); @@ -1103,6 +1134,9 @@ public class BubbleStackView extends FrameLayout * Whether the educational view should show for the expanded view "manage" menu. */ private boolean shouldShowManageEdu() { + if (ActivityManager.isRunningInTestHarness()) { + return false; + } final boolean seen = getPrefBoolean(ManageEducationViewKt.PREF_MANAGED_EDUCATION); final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext)) && mExpandedBubble != null; @@ -1117,16 +1151,19 @@ public class BubbleStackView extends FrameLayout return; } if (mManageEduView == null) { - mManageEduView = new ManageEducationView(mContext); + mManageEduView = new ManageEducationView(mContext, mPositioner); addView(mManageEduView); } - mManageEduView.show(mExpandedBubble.getExpandedView(), mTempRect); + mManageEduView.show(mExpandedBubble.getExpandedView()); } /** * Whether education view should show for the collapsed stack. */ private boolean shouldShowStackEdu() { + if (ActivityManager.isRunningInTestHarness()) { + return false; + } final boolean seen = getPrefBoolean(StackEducationViewKt.PREF_STACK_EDUCATION); final boolean shouldShow = !seen || BubbleDebugConfig.forceShowUserEducation(mContext); if (BubbleDebugConfig.DEBUG_USER_EDUCATION) { @@ -1144,25 +1181,35 @@ public class BubbleStackView extends FrameLayout * @return true if education view for collapsed stack should show and was not showing before. */ private boolean maybeShowStackEdu() { - if (!shouldShowStackEdu()) { + if (!shouldShowStackEdu() || isExpanded()) { return false; } if (mStackEduView == null) { - mStackEduView = new StackEducationView(mContext); + mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); addView(mStackEduView); } mBubbleContainer.bringToFront(); return mStackEduView.show(mPositioner.getDefaultStartPosition()); } + private boolean isStackEduShowing() { + return mStackEduView != null && mStackEduView.getVisibility() == VISIBLE; + } + + // Recreates & shows the education views. Call when a theme/config change happens. private void updateUserEdu() { - maybeShowStackEdu(); - if (mManageEduView != null) { - mManageEduView.invalidate(); + if (isStackEduShowing()) { + removeView(mStackEduView); + mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); + addView(mStackEduView); + mBubbleContainer.bringToFront(); // Stack appears on top of the stack education + mStackEduView.show(mPositioner.getDefaultStartPosition()); } - maybeShowManageEdu(); - if (mStackEduView != null) { - mStackEduView.invalidate(); + if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + removeView(mManageEduView); + mManageEduView = new ManageEducationView(mContext, mPositioner); + addView(mManageEduView); + mManageEduView.show(mExpandedBubble.getExpandedView()); } } @@ -1171,7 +1218,7 @@ public class BubbleStackView extends FrameLayout if (mFlyout != null) { removeView(mFlyout); } - mFlyout = new BubbleFlyoutView(getContext()); + mFlyout = new BubbleFlyoutView(getContext(), mPositioner); mFlyout.setVisibility(GONE); mFlyout.setOnClickListener(mFlyoutClickListener); mFlyout.setOnTouchListener(mFlyoutTouchListener); @@ -1218,6 +1265,10 @@ public class BubbleStackView extends FrameLayout updateOverflow(); updateUserEdu(); updateExpandedViewTheme(); + mScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + mManageMenuScrim.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); } /** @@ -1229,9 +1280,6 @@ public class BubbleStackView extends FrameLayout mRelativeStackPositionBeforeRotation = new RelativeStackPosition( mPositioner.getRestingPosition(), mStackAnimationController.getAllowableStackPositionRegion()); - mManageMenu.setVisibility(View.INVISIBLE); - mShowingManage = false; - addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); } @@ -1255,6 +1303,7 @@ public class BubbleStackView extends FrameLayout setUpManageMenu(); setUpFlyout(); setUpDismissView(); + updateUserEdu(); mBubbleSize = mPositioner.getBubbleSize(); for (Bubble b : mBubbleData.getBubbles()) { if (b.getIconView() == null) { @@ -1292,6 +1341,7 @@ public class BubbleStackView extends FrameLayout @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + mPositioner.update(); getViewTreeObserver().addOnComputeInternalInsetsListener(this); getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); } @@ -1534,7 +1584,8 @@ public class BubbleStackView extends FrameLayout } else { bubble.cleanupViews(); } - updatePointerPosition(); + updatePointerPosition(false /* forIme */); + updateExpandedView(); logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__DISMISSED); return; } @@ -1574,7 +1625,7 @@ public class BubbleStackView extends FrameLayout .map(b -> b.getIconView()).collect(Collectors.toList()); mStackAnimationController.animateReorder(bubbleViews, reorder); } - updatePointerPosition(); + updatePointerPosition(false /* forIme */); } /** @@ -1645,7 +1696,6 @@ public class BubbleStackView extends FrameLayout private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) { final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; - updatePointerPosition(); if (mIsExpanded) { hideCurrentInputMethod(); @@ -1674,6 +1724,7 @@ public class BubbleStackView extends FrameLayout /** * Changes the expanded state of the stack. + * Don't call this directly, call mBubbleData#setExpanded. * * @param shouldExpand whether the bubble stack should appear expanded */ @@ -1710,6 +1761,21 @@ public class BubbleStackView extends FrameLayout notifyExpansionChanged(mExpandedBubble, mIsExpanded); } + /** + * Called when back press occurs while bubbles are expanded. + */ + public void onBackPressed() { + if (mIsExpanded) { + if (mShowingManage) { + showManageMenu(false); + } else if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); + } else { + mBubbleData.setExpanded(false); + } + } + } + void setBubbleVisibility(Bubble b, boolean visible) { if (b.getIconView() != null) { b.getIconView().setVisibility(visible ? VISIBLE : GONE); @@ -1722,6 +1788,7 @@ public class BubbleStackView extends FrameLayout * not. */ void hideCurrentInputMethod() { + mPositioner.setImeVisible(false, 0); mBubbleController.hideCurrentInputMethod(); } @@ -1796,56 +1863,61 @@ public class BubbleStackView extends FrameLayout mExpandedViewAlphaAnimator.start(); } + private void showScrim(boolean show) { + if (show) { + mScrim.animate() + .setInterpolator(ALPHA_IN) + .alpha(SCRIM_ALPHA) + .start(); + } else { + mScrim.animate() + .alpha(0f) + .setInterpolator(ALPHA_OUT) + .start(); + } + } + private void animateExpansion() { cancelDelayedExpandCollapseSwitchAnimations(); final boolean showVertically = mPositioner.showBubblesVertically(); mIsExpanded = true; - if (mStackEduView != null) { + if (isStackEduShowing()) { mStackEduView.hide(true /* fromExpansion */); } beforeExpandedViewAnimation(); + showScrim(true); updateZOrder(); updateBadges(false /* setBadgeForCollapsedStack */); mBubbleContainer.setActiveController(mExpandedAnimationController); updateOverflowVisibility(); - updatePointerPosition(); + updatePointerPosition(false /* forIme */); mExpandedAnimationController.expandFromStack(() -> { if (mIsExpanded && mExpandedBubble.getExpandedView() != null) { maybeShowManageEdu(); } } /* after */); - - if (mPositioner.showingInTaskbar() - // Don't need the scrim when the bar is at the bottom - && mPositioner.getTaskbarPosition() != BubblePositioner.TASKBAR_POSITION_BOTTOM) { - mTaskbarScrim.getLayoutParams().width = mPositioner.getTaskbarSize(); - mTaskbarScrim.setTranslationX(mStackOnLeftOrWillBe - ? 0f - : mPositioner.getAvailableRect().right - mPositioner.getTaskbarSize()); - mTaskbarScrim.setVisibility(VISIBLE); - mTaskbarScrim.animate().alpha(1f).start(); - } - - mExpandedViewContainer.setTranslationX(0f); - mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); - mExpandedViewContainer.setAlpha(1f); - int index; if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { index = mBubbleData.getBubbles().size(); } else { index = getBubbleIndex(mExpandedBubble); } - // Position of the bubble we're expanding, once it's settled in its row. - final float bubbleWillBeAt = - mExpandedAnimationController.getBubbleXOrYForOrientation(index); + PointF p = mPositioner.getExpandedBubbleXY(index, getState()); + final float translationY = mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? p.y : p.x); + mExpandedViewContainer.setTranslationX(0f); + mExpandedViewContainer.setTranslationY(translationY); + mExpandedViewContainer.setAlpha(1f); // How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles // that are animating farther, so that the expanded view doesn't move as much. final float relevantStackPosition = showVertically ? mStackAnimationController.getStackPosition().y : mStackAnimationController.getStackPosition().x; + final float bubbleWillBeAt = showVertically + ? p.y + : p.x; final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition); // Wait for the path animation target to reach its end, and add a small amount of extra time @@ -1862,27 +1934,27 @@ public class BubbleStackView extends FrameLayout // Set the pivot point for the scale, so the expanded view animates out from the bubble. if (showVertically) { float pivotX; - float pivotY = bubbleWillBeAt + mBubbleSize / 2f; if (mStackOnLeftOrWillBe) { - pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + pivotX = p.x + mBubbleSize + mExpandedViewPadding; } else { - pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; + pivotX = p.x - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - pivotX, pivotY); + pivotX, + p.y + mBubbleSize / 2f); } else { mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - bubbleWillBeAt + mBubbleSize / 2f, - mPositioner.getExpandedViewY()); + p.x + mBubbleSize / 2f, + p.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); if (mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.setExpandedContentAlpha(0f); + mExpandedBubble.getExpandedView().setTaskViewAlpha(0f); // We'll be starting the alpha animation after a slight delay, so set this flag early // here. @@ -1914,6 +1986,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainerMatrix); }) .withEndActions(() -> { + mExpandedViewContainer.setAnimationMatrix(null); afterExpandedViewAnimation(); if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { @@ -1929,12 +2002,17 @@ public class BubbleStackView extends FrameLayout private void animateCollapse() { cancelDelayedExpandCollapseSwitchAnimations(); + if (mManageEduView != null && mManageEduView.getVisibility() == VISIBLE) { + mManageEduView.hide(); + } // Hide the menu if it's visible. showManageMenu(false); mIsExpanded = false; mIsExpansionAnimating = true; + showScrim(false); + mBubbleContainer.cancelAllAnimations(); // If we were in the middle of swapping, the animating-out surface would have been scaling @@ -1952,10 +2030,6 @@ public class BubbleStackView extends FrameLayout /* collapseTo */, () -> mBubbleContainer.setActiveController(mStackAnimationController)); - if (mTaskbarScrim.getVisibility() == VISIBLE) { - mTaskbarScrim.animate().alpha(0f).start(); - } - int index; if (mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey())) { index = mBubbleData.getBubbles().size(); @@ -1963,12 +2037,10 @@ public class BubbleStackView extends FrameLayout index = mBubbleData.getBubbles().indexOf(mExpandedBubble); } // Value the bubble is animating from (back into the stack). - final float expandingFromBubbleAt = - mExpandedAnimationController.getBubbleXOrYForOrientation(index); - final boolean showVertically = mPositioner.showBubblesVertically(); + final PointF p = mPositioner.getExpandedBubbleXY(index, getState()); if (mPositioner.showBubblesVertically()) { float pivotX; - float pivotY = expandingFromBubbleAt + mBubbleSize / 2f; + float pivotY = p.y + mBubbleSize / 2f; if (mStackOnLeftOrWillBe) { pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; } else { @@ -1980,8 +2052,8 @@ public class BubbleStackView extends FrameLayout } else { mExpandedViewContainerMatrix.setScale( 1f, 1f, - expandingFromBubbleAt + mBubbleSize / 2f, - mPositioner.getExpandedViewY()); + p.x + mBubbleSize / 2f, + p.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewAlphaAnimator.reverse(); @@ -2008,7 +2080,7 @@ public class BubbleStackView extends FrameLayout final BubbleViewProvider previouslySelected = mExpandedBubble; beforeExpandedViewAnimation(); if (mManageEduView != null) { - mManageEduView.hide(false /* fromExpansion */); + mManageEduView.hide(); } if (DEBUG_BUBBLE_STACK_VIEW) { @@ -2023,10 +2095,6 @@ public class BubbleStackView extends FrameLayout if (previouslySelected != null) { previouslySelected.setTaskViewVisibility(false); } - - if (mPositioner.showingInTaskbar()) { - mTaskbarScrim.setVisibility(GONE); - } }) .start(); } @@ -2063,32 +2131,31 @@ public class BubbleStackView extends FrameLayout boolean isOverflow = mExpandedBubble != null && mExpandedBubble.getKey().equals(BubbleOverflow.KEY); - float expandingFromBubbleDestination = - mExpandedAnimationController.getBubbleXOrYForOrientation(isOverflow - ? getBubbleCount() - : mBubbleData.getBubbles().indexOf(mExpandedBubble)); - + PointF p = mPositioner.getExpandedBubbleXY(isOverflow + ? mBubbleContainer.getChildCount() - 1 + : mBubbleData.getBubbles().indexOf(mExpandedBubble), + getState()); mExpandedViewContainer.setAlpha(1f); mExpandedViewContainer.setVisibility(View.VISIBLE); if (mPositioner.showBubblesVertically()) { float pivotX; - float pivotY = expandingFromBubbleDestination + mBubbleSize / 2f; + float pivotY = p.y + mBubbleSize / 2f; if (mStackOnLeftOrWillBe) { - pivotX = mPositioner.getAvailableRect().left + mBubbleSize + mExpandedViewPadding; + pivotX = p.x + mBubbleSize + mExpandedViewPadding; } else { - pivotX = mPositioner.getAvailableRect().right - mBubbleSize - mExpandedViewPadding; - + pivotX = p.x - mExpandedViewPadding; } mExpandedViewContainerMatrix.setScale( - 0f, 0f, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, pivotX, pivotY); } else { mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - expandingFromBubbleDestination + mBubbleSize / 2f, - mPositioner.getExpandedViewY()); + p.x + mBubbleSize / 2f, + p.y + mBubbleSize + mExpandedViewPadding); } mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); @@ -2113,6 +2180,7 @@ public class BubbleStackView extends FrameLayout .withEndActions(() -> { mExpandedViewTemporarilyHidden = false; mIsBubbleSwitchAnimating = false; + mExpandedViewContainer.setAnimationMatrix(null); }) .start(); }, 25); @@ -2144,9 +2212,20 @@ public class BubbleStackView extends FrameLayout } } - /** Moves the bubbles out of the way if they're going to be over the keyboard. */ - public void onImeVisibilityChanged(boolean visible, int height) { - mStackAnimationController.setImeHeight(visible ? height + mImeOffset : 0); + /** + * Updates the stack based for IME changes. When collapsed it'll move the stack if it + * overlaps where they IME would be. When expanded it'll shift the expanded bubbles + * if they might overlap with the IME (this only happens for large screens). + */ + public void animateForIme(boolean visible) { + if ((mIsExpansionAnimating || mIsBubbleSwitchAnimating) && mIsExpanded) { + // This will update the animation so the bubbles move to position for the IME + mExpandedAnimationController.expandFromStack(() -> { + updatePointerPosition(false /* forIme */); + afterExpandedViewAnimation(); + } /* after */); + return; + } if (!mIsExpanded && getBubbleCount() > 0) { final float stackDestinationY = @@ -2165,9 +2244,20 @@ public class BubbleStackView extends FrameLayout FLYOUT_IME_ANIMATION_SPRING_CONFIG) .start(); } - } else if (mIsExpanded && mExpandedBubble != null - && mExpandedBubble.getExpandedView() != null) { + } else if (mPositioner.showBubblesVertically() && mIsExpanded + && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { mExpandedBubble.getExpandedView().setImeVisible(visible); + List<Animator> animList = new ArrayList(); + for (int i = 0; i < mBubbleContainer.getChildCount(); i++) { + View child = mBubbleContainer.getChildAt(i); + float transY = mPositioner.getExpandedBubbleXY(i, getState()).y; + ObjectAnimator anim = ObjectAnimator.ofFloat(child, TRANSLATION_Y, transY); + animList.add(anim); + } + updatePointerPosition(true /* forIme */); + AnimatorSet set = new AnimatorSet(); + set.playTogether(animList); + set.start(); } } @@ -2329,7 +2419,7 @@ public class BubbleStackView extends FrameLayout if (flyoutMessage == null || flyoutMessage.message == null || !bubble.showFlyout() - || (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) + || isStackEduShowing() || isExpanded() || mIsExpansionAnimating || mIsGestureInProgress @@ -2403,20 +2493,20 @@ public class BubbleStackView extends FrameLayout if (mFlyout.getVisibility() == View.VISIBLE) { - mFlyout.animateUpdate(bubble.getFlyoutMessage(), getWidth(), + mFlyout.animateUpdate(bubble.getFlyoutMessage(), mStackAnimationController.getStackPosition(), !bubble.showDot(), + bubble.getIconView().getDotCenter(), mAfterFlyoutHidden /* onHide */); } else { mFlyout.setVisibility(INVISIBLE); mFlyout.setupFlyoutStartingAsDot(bubble.getFlyoutMessage(), - mStackAnimationController.getStackPosition(), getWidth(), + mStackAnimationController.getStackPosition(), mStackAnimationController.isStackOnLeftSide(), bubble.getIconView().getDotColor() /* dotColor */, expandFlyoutAfterDelay /* onLayoutComplete */, mAfterFlyoutHidden /* onHide */, bubble.getIconView().getDotCenter(), - !bubble.showDot(), - mPositioner); + !bubble.showDot()); } mFlyout.bringToFront(); }); @@ -2452,7 +2542,7 @@ public class BubbleStackView extends FrameLayout * them. */ public void getTouchableRegion(Rect outRect) { - if (mStackEduView != null && mStackEduView.getVisibility() == VISIBLE) { + if (isStackEduShowing()) { // When user education shows then capture all touches outRect.set(0, 0, getWidth(), getHeight()); return; @@ -2472,7 +2562,7 @@ public class BubbleStackView extends FrameLayout // Account for the IME in the touchable region so that the touchable region of the // Bubble window doesn't obscure the IME. The touchable region affects which areas // of the screen can be excluded by lower windows (IME is just above the embedded task) - outRect.bottom -= (int) mStackAnimationController.getImeHeight(); + outRect.bottom -= mPositioner.getImeHeight(); } if (mFlyout.getVisibility() == View.VISIBLE) { @@ -2491,15 +2581,36 @@ public class BubbleStackView extends FrameLayout invalidate(); } - private void showManageMenu(boolean show) { + /** Hide or show the manage menu for the currently expanded bubble. */ + @VisibleForTesting + public void showManageMenu(boolean show) { mShowingManage = show; // This should not happen, since the manage menu is only visible when there's an expanded // bubble. If we end up in this state, just hide the menu immediately. if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { mManageMenu.setVisibility(View.INVISIBLE); + mManageMenuScrim.setVisibility(INVISIBLE); + mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */); return; } + if (show) { + mManageMenuScrim.setVisibility(VISIBLE); + mManageMenuScrim.setTranslationZ(mManageMenu.getElevation() - 1f); + } + Runnable endAction = () -> { + if (!show) { + mManageMenuScrim.setVisibility(INVISIBLE); + mManageMenuScrim.setTranslationZ(0f); + } + }; + + mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show); + mManageMenuScrim.animate() + .setInterpolator(show ? ALPHA_IN : ALPHA_OUT) + .alpha(show ? SCRIM_ALPHA : 0f) + .withEndAction(endAction) + .start(); // If available, update the manage menu's settings option with the expanded bubble's app // name and icon. @@ -2510,7 +2621,6 @@ public class BubbleStackView extends FrameLayout R.string.bubbles_app_settings, bubble.getAppName())); } - mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); if (mExpandedBubble.getExpandedView().getTaskView() != null) { mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage ? new Rect(0, 0, getWidth(), getHeight()) @@ -2522,7 +2632,11 @@ public class BubbleStackView extends FrameLayout // When the menu is open, it should be at these coordinates. The menu pops out to the right // in LTR and to the left in RTL. - final float targetX = isLtr ? mTempRect.left : mTempRect.right - mManageMenu.getWidth(); + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); + final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin(); + final float targetX = isLtr + ? mTempRect.left - margin + : mTempRect.right + margin - mManageMenu.getWidth(); final float targetY = mTempRect.bottom - mManageMenu.getHeight(); final float xOffsetForAnimation = (isLtr ? 1 : -1) * mManageMenu.getWidth() / 4f; @@ -2702,18 +2816,21 @@ public class BubbleStackView extends FrameLayout } boolean isOverflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()); - int[] paddings = mPositioner.getExpandedViewPadding( + int[] paddings = mPositioner.getExpandedViewContainerPadding( mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded); mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]); if (mIsExpansionAnimating) { mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE); } if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY()); + PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble), + getState()); + mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble, + mPositioner.showBubblesVertically() ? p.y : p.x)); mExpandedViewContainer.setTranslationX(0f); mExpandedBubble.getExpandedView().updateView( mExpandedViewContainer.getLocationOnScreen()); - updatePointerPosition(); + updatePointerPosition(false /* forIme */); } mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide(); @@ -2784,7 +2901,13 @@ public class BubbleStackView extends FrameLayout } } - private void updatePointerPosition() { + /** + * Updates the position of the pointer based on the expanded bubble. + * + * @param forIme whether the position is being updated due to the ime appearing, in this case + * the pointer is animated to the location. + */ + private void updatePointerPosition(boolean forIme) { if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { return; } @@ -2792,8 +2915,12 @@ public class BubbleStackView extends FrameLayout if (index == -1) { return; } - float bubblePosition = mExpandedAnimationController.getBubbleXOrYForOrientation(index); - mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, mStackOnLeftOrWillBe); + PointF position = mPositioner.getExpandedBubbleXY(index, getState()); + float bubblePosition = mPositioner.showBubblesVertically() + ? position.y + : position.x; + mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition, + mStackOnLeftOrWillBe, forIme /* animate */); } /** @@ -2876,6 +3003,26 @@ public class BubbleStackView extends FrameLayout return bubbles; } + /** @return the current stack state. */ + public StackViewState getState() { + mStackViewState.numberOfBubbles = mBubbleContainer.getChildCount(); + mStackViewState.selectedIndex = getBubbleIndex(mExpandedBubble); + mStackViewState.onLeft = mStackOnLeftOrWillBe; + return mStackViewState; + } + + /** + * Holds some commonly queried information about the stack. + */ + public static class StackViewState { + // Number of bubbles (including the overflow itself) in the stack. + public int numberOfBubbles; + // The selected index if the stack is expanded. + public int selectedIndex; + // Whether the stack is resting on the left or right side of the screen when collapsed. + public boolean onLeft; + } + /** * Representation of stack position that uses relative properties rather than absolute * coordinates. This is used to maintain similar stack positions across configuration changes. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java index 38b3ba9dfda0..7e552826e94a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewProvider.java @@ -29,12 +29,6 @@ public interface BubbleViewProvider { @Nullable BubbleExpandedView getExpandedView(); /** - * Sets the alpha of the expanded view content. This will be applied to both the expanded view - * container itself (the manage button, etc.) as well as the TaskView within it. - */ - void setExpandedContentAlpha(float alpha); - - /** * Sets whether the contents of the bubble's TaskView should be visible. */ void setTaskViewVisibility(boolean visible); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index c73b5eebc5c2..c82249b8a369 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -24,12 +24,10 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.os.Bundle; -import android.os.Looper; import android.service.notification.NotificationListenerService.RankingMap; import android.util.ArraySet; import android.util.Pair; import android.util.SparseArray; -import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.Nullable; @@ -43,7 +41,6 @@ import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; import java.util.concurrent.Executor; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -160,14 +157,6 @@ public interface Bubbles { /** Set the proxy to commnuicate with SysUi side components. */ void setSysuiProxy(SysuiProxy proxy); - /** - * Set the scrim view for bubbles. - * - * @param callback The callback made with the executor and the executor's looper that the view - * will be running on. - **/ - void setBubbleScrim(View view, BiConsumer<Executor, Looper> callback); - /** Set a listener to be notified of bubble expand events. */ void setExpandListener(BubbleExpandListener listener); @@ -295,6 +284,8 @@ public interface Bubbles { void onStackExpandChanged(boolean shouldExpand); + void onManageMenuExpandChanged(boolean menuExpanded); + void onUnbubbleConversation(String key); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt index 0a1cd2246339..74672a336161 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DismissView.kt @@ -28,37 +28,40 @@ import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW import com.android.wm.shell.R import com.android.wm.shell.animation.PhysicsAnimator import com.android.wm.shell.common.DismissCircleView +import android.view.WindowInsets +import android.view.WindowManager /* * View that handles interactions between DismissCircleView and BubbleStackView. */ class DismissView(context: Context) : FrameLayout(context) { - var circle = DismissCircleView(context).apply { - val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) - val newParams = LayoutParams(targetSize, targetSize) - newParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL - setLayoutParams(newParams) - setTranslationY( - resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height).toFloat()) - } - + var circle = DismissCircleView(context) var isShowing = false + private val animator = PhysicsAnimator.getInstance(circle) private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) private val DISMISS_SCRIM_FADE_MS = 200 + private var wm: WindowManager = + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager init { setLayoutParams(LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, resources.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height), Gravity.BOTTOM)) - setPadding(0, 0, 0, resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) + updatePadding() setClipToPadding(false) setClipChildren(false) setVisibility(View.INVISIBLE) setBackgroundResource( R.drawable.floating_dismiss_gradient_transition) - addView(circle) + + val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + addView(circle, LayoutParams(targetSize, targetSize, + Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL)) + // start with circle offscreen so it's animated up + circle.setTranslationY(resources.getDimensionPixelSize( + R.dimen.floating_dismiss_gradient_height).toFloat()) } /** @@ -91,9 +94,21 @@ class DismissView(context: Context) : FrameLayout(context) { } fun updateResources() { - val targetSize: Int = context.resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) + updatePadding() + layoutParams.height = resources.getDimensionPixelSize( + R.dimen.floating_dismiss_gradient_height) + + val targetSize: Int = resources.getDimensionPixelSize(R.dimen.dismiss_circle_size) circle.layoutParams.width = targetSize circle.layoutParams.height = targetSize circle.requestLayout() } + + private fun updatePadding() { + val insets: WindowInsets = wm.getCurrentWindowMetrics().getWindowInsets() + val navInset = insets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars()) + setPadding(0, 0, 0, navInset.bottom + + resources.getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin)) + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt index 4cc67025fff4..eb4737ac6c63 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ManageEducationView.kt @@ -18,12 +18,13 @@ package com.android.wm.shell.bubbles import android.content.Context import android.graphics.Color import android.graphics.Rect +import android.graphics.drawable.ColorDrawable import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout -import android.widget.TextView -import com.android.internal.util.ContrastColorUtil +import com.android.internal.R.color.system_neutral1_900 import com.android.wm.shell.R import com.android.wm.shell.animation.Interpolators @@ -31,21 +32,22 @@ import com.android.wm.shell.animation.Interpolators * User education view to highlight the manage button that allows a user to configure the settings * for the bubble. Shown only the first time a user expands a bubble. */ -class ManageEducationView constructor(context: Context) : LinearLayout(context) { +class ManageEducationView constructor(context: Context, positioner: BubblePositioner) + : LinearLayout(context) { - private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleManageEducationView" + private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "ManageEducationView" else BubbleDebugConfig.TAG_BUBBLES private val ANIMATE_DURATION: Long = 200 - private val ANIMATE_DURATION_SHORT: Long = 40 - private val manageView by lazy { findViewById<View>(R.id.manage_education_view) } - private val manageButton by lazy { findViewById<Button>(R.id.manage) } + private val positioner: BubblePositioner = positioner + private val manageView by lazy { findViewById<ViewGroup>(R.id.manage_education_view) } + private val manageButton by lazy { findViewById<Button>(R.id.manage_button) } private val gotItButton by lazy { findViewById<Button>(R.id.got_it) } - private val titleTextView by lazy { findViewById<TextView>(R.id.user_education_title) } - private val descTextView by lazy { findViewById<TextView>(R.id.user_education_description) } private var isHiding = false + private var realManageButtonRect = Rect() + private var bubbleExpandedView: BubbleExpandedView? = null init { LayoutInflater.from(context).inflate(R.layout.bubbles_manage_button_education, this) @@ -66,18 +68,17 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) override fun onFinishInflate() { super.onFinishInflate() layoutDirection = resources.configuration.layoutDirection - setTextColor() } - private fun setTextColor() { - val typedArray = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, - android.R.attr.textColorPrimaryInverse)) - val bgColor = typedArray.getColor(0 /* index */, Color.BLACK) - var textColor = typedArray.getColor(1 /* index */, Color.WHITE) + private fun setButtonColor() { + val typedArray = mContext.obtainStyledAttributes(intArrayOf( + com.android.internal.R.attr.colorAccentPrimary)) + val buttonColor = typedArray.getColor(0 /* index */, Color.TRANSPARENT) typedArray.recycle() - textColor = ContrastColorUtil.ensureTextContrast(textColor, bgColor, true) - titleTextView.setTextColor(textColor) - descTextView.setTextColor(textColor) + + manageButton.setTextColor(mContext.getColor(system_neutral1_900)) + manageButton.setBackgroundDrawable(ColorDrawable(buttonColor)) + gotItButton.setBackgroundDrawable(ColorDrawable(buttonColor)) } private fun setDrawableDirection() { @@ -91,30 +92,39 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) * If necessary, toggles the user education view for the manage button. This is shown when the * bubble stack is expanded for the first time. * - * @param show whether the user education view should show or not. + * @param expandedView the expandedView the user education is shown on top of. */ - fun show(expandedView: BubbleExpandedView, rect: Rect) { + fun show(expandedView: BubbleExpandedView) { + setButtonColor() if (visibility == VISIBLE) return + bubbleExpandedView = expandedView + expandedView.taskView?.setObscuredTouchRect(Rect(positioner.screenRect)) + + layoutParams.width = if (positioner.isLargeScreen) + context.resources.getDimensionPixelSize( + R.dimen.bubbles_user_education_width_large_screen) + else ViewGroup.LayoutParams.MATCH_PARENT + alpha = 0f visibility = View.VISIBLE + expandedView.getManageButtonBoundsOnScreen(realManageButtonRect) + manageView.setPadding(realManageButtonRect.left - expandedView.manageButtonMargin, + manageView.paddingTop, manageView.paddingRight, manageView.paddingBottom) post { - expandedView.getManageButtonBoundsOnScreen(rect) - manageButton .setOnClickListener { - expandedView.findViewById<View>(R.id.settings_button).performClick() - hide(true /* isStackExpanding */) + hide() + expandedView.findViewById<View>(R.id.manage_button).performClick() } - gotItButton.setOnClickListener { hide(true /* isStackExpanding */) } - setOnClickListener { hide(true /* isStackExpanding */) } - - with(manageView) { - translationX = 0f - val inset = resources.getDimensionPixelSize( - R.dimen.bubbles_manage_education_top_inset) - translationY = (rect.top - manageView.height + inset).toFloat() - } + gotItButton.setOnClickListener { hide() } + setOnClickListener { hide() } + + val offsetViewBounds = Rect() + manageButton.getDrawingRect(offsetViewBounds) + manageView.offsetDescendantRectToMyCoords(manageButton, offsetViewBounds) + translationX = 0f + translationY = (realManageButtonRect.top - offsetViewBounds.top).toFloat() bringToFront() animate() .setDuration(ANIMATE_DURATION) @@ -124,13 +134,14 @@ class ManageEducationView constructor(context: Context) : LinearLayout(context) setShouldShow(false) } - fun hide(isStackExpanding: Boolean) { + fun hide() { + bubbleExpandedView?.taskView?.setObscuredTouchRect(null) if (visibility != VISIBLE || isHiding) return animate() .withStartAction { isHiding = true } .alpha(0f) - .setDuration(if (isStackExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) + .setDuration(ANIMATE_DURATION) .withEndAction { isHiding = false visibility = GONE diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS new file mode 100644 index 000000000000..8271014d290e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module bubble owner +madym@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index 0a2cfc4089ed..3846de73842d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -18,8 +18,11 @@ package com.android.wm.shell.bubbles import android.content.Context import android.graphics.Color import android.graphics.PointF +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View +import android.view.View.OnKeyListener +import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import com.android.internal.util.ContrastColorUtil @@ -30,7 +33,12 @@ import com.android.wm.shell.animation.Interpolators * User education view to highlight the collapsed stack of bubbles. * Shown only the first time a user taps the stack. */ -class StackEducationView constructor(context: Context) : LinearLayout(context) { +class StackEducationView constructor( + context: Context, + positioner: BubblePositioner, + controller: BubbleController +) + : LinearLayout(context) { private val TAG = if (BubbleDebugConfig.TAG_WITH_CLASS_NAME) "BubbleStackEducationView" else BubbleDebugConfig.TAG_BUBBLES @@ -38,6 +46,9 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { private val ANIMATE_DURATION: Long = 200 private val ANIMATE_DURATION_SHORT: Long = 40 + private val positioner: BubblePositioner = positioner + private val controller: BubbleController = controller + private val view by lazy { findViewById<View>(R.id.stack_education_layout) } private val titleTextView by lazy { findViewById<TextView>(R.id.stack_education_title) } private val descTextView by lazy { findViewById<TextView>(R.id.stack_education_description) } @@ -67,6 +78,28 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { setTextColor() } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + setFocusableInTouchMode(true) + setOnKeyListener(object : OnKeyListener { + override fun onKey(v: View?, keyCode: Int, event: KeyEvent): Boolean { + // if the event is a key down event on the enter button + if (event.action == KeyEvent.ACTION_UP && + keyCode == KeyEvent.KEYCODE_BACK && !isHiding) { + hide(false) + return true + } + return false + } + }) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + setOnKeyListener(null) + controller.updateWindowFlagsForBackpress(false /* interceptBack */) + } + private fun setTextColor() { val ta = mContext.obtainStyledAttributes(intArrayOf(android.R.attr.colorAccent, android.R.attr.textColorPrimaryInverse)) @@ -92,15 +125,28 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { * @return true if user education was shown, false otherwise. */ fun show(stackPosition: PointF): Boolean { + isHiding = false if (visibility == VISIBLE) return false + controller.updateWindowFlagsForBackpress(true /* interceptBack */) + layoutParams.width = if (positioner.isLargeScreen) + context.resources.getDimensionPixelSize( + R.dimen.bubbles_user_education_width_large_screen) + else ViewGroup.LayoutParams.MATCH_PARENT + setAlpha(0f) setVisibility(View.VISIBLE) post { + requestFocus() with(view) { - val bubbleSize = context.resources.getDimensionPixelSize( - R.dimen.bubble_size) - translationY = stackPosition.y + bubbleSize / 2 - getHeight() / 2 + if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + setPadding(positioner.bubbleSize + paddingRight, paddingTop, paddingRight, + paddingBottom) + } else { + setPadding(paddingLeft, paddingTop, positioner.bubbleSize + paddingLeft, + paddingBottom) + } + translationY = stackPosition.y + positioner.bubbleSize / 2 - getHeight() / 2 } animate() .setDuration(ANIMATE_DURATION) @@ -114,15 +160,17 @@ class StackEducationView constructor(context: Context) : LinearLayout(context) { /** * If necessary, hides the stack education view. * - * @param fromExpansion if true this indicates the hide is happening due to the bubble being + * @param isExpanding if true this indicates the hide is happening due to the bubble being * expanded, false if due to a touch outside of the bubble stack. */ - fun hide(fromExpansion: Boolean) { + fun hide(isExpanding: Boolean) { if (visibility != VISIBLE || isHiding) return + isHiding = true + controller.updateWindowFlagsForBackpress(false /* interceptBack */) animate() .alpha(0f) - .setDuration(if (fromExpansion) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) + .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) .withEndAction { visibility = GONE } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java index df2b440c19df..19d513f81cab 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java @@ -21,7 +21,6 @@ import static com.android.wm.shell.bubbles.BubblePositioner.NUM_VISIBLE_WHEN_RES import android.content.res.Resources; import android.graphics.Path; import android.graphics.PointF; -import android.graphics.Rect; import android.view.View; import androidx.annotation.NonNull; @@ -33,6 +32,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.animation.PhysicsAnimator; import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.bubbles.BubbleStackView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import com.google.android.collect.Sets; @@ -64,9 +64,6 @@ public class ExpandedAnimationController /** Stiffness for the expand/collapse path-following animation. */ private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000; - /** What percentage of the screen to use when centering the bubbles in landscape. */ - private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f; - /** * Velocity required to dismiss an individual bubble without dragging it into the dismiss * target. @@ -79,16 +76,8 @@ public class ExpandedAnimationController /** Horizontal offset between bubbles, which we need to know to re-stack them. */ private float mStackOffsetPx; - /** Space between status bar and bubbles in the expanded state. */ - private float mBubblePaddingTop; /** Size of each bubble. */ private float mBubbleSizePx; - /** Max number of bubbles shown in row above expanded view. */ - private int mBubblesMaxRendered; - /** Max amount of space to have between bubbles when expanded. */ - private int mBubblesMaxSpace; - /** Amount of space between the bubbles when expanded. */ - private float mSpaceBetweenBubbles; /** Whether the expand / collapse animation is running. */ private boolean mAnimatingExpand = false; @@ -127,8 +116,6 @@ public class ExpandedAnimationController /** The bubble currently being dragged out of the row (to potentially be dismissed). */ private MagnetizedObject<View> mMagnetizedBubbleDraggingOut; - private int mExpandedViewPadding; - /** * Callback to run whenever any bubble is animated out. The BubbleStackView will check if the * end of this animation means we have no bubbles left, and notify the BubbleController. @@ -137,13 +124,15 @@ public class ExpandedAnimationController private BubblePositioner mPositioner; - public ExpandedAnimationController(BubblePositioner positioner, int expandedViewPadding, - Runnable onBubbleAnimatedOutAction) { + private BubbleStackView mBubbleStackView; + + public ExpandedAnimationController(BubblePositioner positioner, + Runnable onBubbleAnimatedOutAction, BubbleStackView stackView) { mPositioner = positioner; updateResources(); - mExpandedViewPadding = expandedViewPadding; mOnBubbleAnimatedOutAction = onBubbleAnimatedOutAction; mCollapsePoint = mPositioner.getDefaultStartPosition(); + mBubbleStackView = stackView; } /** @@ -208,11 +197,8 @@ public class ExpandedAnimationController return; } Resources res = mLayout.getContext().getResources(); - mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); mStackOffsetPx = res.getDimensionPixelSize(R.dimen.bubble_stack_offset); mBubbleSizePx = mPositioner.getBubbleSize(); - mBubblesMaxRendered = mPositioner.getMaxBubbles(); - mSpaceBetweenBubbles = res.getDimensionPixelSize(R.dimen.bubble_spacing); } /** @@ -256,31 +242,19 @@ public class ExpandedAnimationController final Path path = new Path(); path.moveTo(bubble.getTranslationX(), bubble.getTranslationY()); - final float expandedY = mPositioner.showBubblesVertically() - ? getBubbleXOrYForOrientation(index) - : getExpandedY(); + final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); if (expanding) { - // If we're expanding, first draw a line from the bubble's current position to the - // top of the screen. - path.lineTo(bubble.getTranslationX(), expandedY); + // If we're expanding, first draw a line from the bubble's current position to where + // it'll end up + path.lineTo(bubble.getTranslationX(), p.y); // Then, draw a line across the screen to the bubble's resting position. - if (mPositioner.showBubblesVertically()) { - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); - float translationX = onLeft - ? availableRect.left - : availableRect.right - mBubbleSizePx; - path.lineTo(translationX, getBubbleXOrYForOrientation(index)); - } else { - path.lineTo(getBubbleXOrYForOrientation(index), expandedY); - } + path.lineTo(p.x, p.y); } else { final float stackedX = mCollapsePoint.x; // If we're collapsing, draw a line from the bubble's current position to the side // of the screen where the bubble will be stacked. - path.lineTo(stackedX, expandedY); + path.lineTo(stackedX, p.y); // Then, draw a line down to the stack position. path.lineTo(stackedX, mCollapsePoint.y @@ -372,6 +346,9 @@ public class ExpandedAnimationController * bubble is dragged back into the row. */ public void dragBubbleOut(View bubbleView, float x, float y) { + if (mMagnetizedBubbleDraggingOut == null) { + return; + } if (mSpringToTouchOnNextMotionEvent) { springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y); mSpringToTouchOnNextMotionEvent = false; @@ -390,8 +367,9 @@ public class ExpandedAnimationController bubbleView.setTranslationY(y); } + final float expandedY = mPositioner.getExpandedViewYTopAligned(); final boolean draggedOutEnough = - y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; + y > expandedY + mBubbleSizePx || y < expandedY - mBubbleSizePx; if (draggedOutEnough != mBubbleDraggedOutEnough) { updateBubblePositions(); mBubbleDraggedOutEnough = draggedOutEnough; @@ -435,9 +413,9 @@ public class ExpandedAnimationController return; } final int index = mLayout.indexOfChild(bubbleView); - + final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); animationForChildAtIndex(index) - .position(getBubbleXOrYForOrientation(index), getExpandedY()) + .position(p.x, p.y) .withPositionStartVelocities(velX, velY) .start(() -> bubbleView.setTranslationZ(0f) /* after */); @@ -453,20 +431,6 @@ public class ExpandedAnimationController updateBubblePositions(); } - /** - * Animates the bubbles to {@link #getExpandedY()} position. Used in response to IME showing. - */ - public void updateYPosition(Runnable after) { - if (mLayout == null) return; - animationsForChildrenFromIndex( - 0, (i, anim) -> anim.translationY(getExpandedY())).startAll(after); - } - - /** The Y value of the row of expanded bubbles. */ - public float getExpandedY() { - return mPositioner.getAvailableRect().top + mBubblePaddingTop; - } - /** Description of current animation controller state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("ExpandedAnimationController state:"); @@ -522,37 +486,35 @@ public class ExpandedAnimationController startOrUpdatePathAnimation(true /* expanding */); } else if (mAnimatingCollapse) { startOrUpdatePathAnimation(false /* expanding */); - } else if (mPositioner.showBubblesVertically()) { - child.setTranslationY(getBubbleXOrYForOrientation(index)); - if (!mPreparingToCollapse) { - // Only animate if we're not collapsing as that animation will handle placing the + } else { + boolean onLeft = mPositioner.isStackOnLeft(mCollapsePoint); + final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState()); + if (mPositioner.showBubblesVertically()) { + child.setTranslationY(p.y); + } else { + child.setTranslationX(p.x); + } + + if (mPreparingToCollapse) { + // Don't animate if we're collapsing, as that animation will handle placing the // new bubble in the stacked position. - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); + return; + } + + if (mPositioner.showBubblesVertically()) { float fromX = onLeft - ? -mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR - : availableRect.right + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; - float toX = onLeft - ? availableRect.left + mExpandedViewPadding - : availableRect.right - mBubbleSizePx - mExpandedViewPadding; + ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR + : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationX(fromX, toX) + .translationX(fromX, p.y) .start(); - updateBubblePositions(); - } - } else { - child.setTranslationX(getBubbleXOrYForOrientation(index)); - if (!mPreparingToCollapse) { - // Only animate if we're not collapsing as that animation will handle placing the - // new bubble in the stacked position. - float toY = getExpandedY(); - float fromY = getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; + } else { + float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR; animationForChild(child) - .translationY(fromY, toY) + .translationY(fromY, p.y) .start(); - updateBubblePositions(); } + updateBubblePositions(); } } @@ -599,7 +561,6 @@ public class ExpandedAnimationController if (mAnimatingExpand || mAnimatingCollapse) { return; } - for (int i = 0; i < mLayout.getChildCount(); i++) { final View bubble = mLayout.getChildAt(i); @@ -609,49 +570,11 @@ public class ExpandedAnimationController return; } - if (mPositioner.showBubblesVertically()) { - Rect availableRect = mPositioner.getAvailableRect(); - boolean onLeft = mCollapsePoint != null - && mCollapsePoint.x < (availableRect.width() / 2f); - animationForChild(bubble) - .translationX(onLeft - ? availableRect.left - : availableRect.right - mBubbleSizePx) - .translationY(getBubbleXOrYForOrientation(i)) - .start(); - } else { - animationForChild(bubble) - .translationX(getBubbleXOrYForOrientation(i)) - .translationY(getExpandedY()) - .start(); - } - } - } - - // TODO - could move to method on bubblePositioner if mSpaceBetweenBubbles gets moved - /** - * When bubbles are expanded in portrait, they display at the top of the screen in a horizontal - * row. When in landscape or on a large screen, they show at the left or right side in a - * vertical row. This method accounts for screen orientation and will return an x or y value - * for the position of the bubble in the row. - * - * @param index Bubble index in row. - * @return the y position of the bubble if showing vertically and the x position if showing - * horizontally. - */ - public float getBubbleXOrYForOrientation(int index) { - if (mLayout == null) { - return 0; + final PointF p = mPositioner.getExpandedBubbleXY(i, mBubbleStackView.getState()); + animationForChild(bubble) + .translationX(p.x) + .translationY(p.y) + .start(); } - final float positionInBar = index * (mBubbleSizePx + mSpaceBetweenBubbles); - Rect availableRect = mPositioner.getAvailableRect(); - final boolean isLandscape = mPositioner.showBubblesVertically(); - final float expandedStackSize = (mLayout.getChildCount() * mBubbleSizePx) - + ((mLayout.getChildCount() - 1) * mSpaceBetweenBubbles); - final float centerPosition = isLandscape - ? availableRect.centerY() - : availableRect.centerX(); - final float rowStart = centerPosition - (expandedStackSize / 2f); - return rowStart + positionInBar; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index 636e1452aa9b..60b64333114e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -127,9 +127,6 @@ public class StackAnimationController extends /** Whether or not the stack's start position has been set. */ private boolean mStackMovedToStartPosition = false; - /** The height of the most recently visible IME. */ - private float mImeHeight = 0f; - /** * The Y position of the stack before the IME became visible, or {@link Float#MIN_VALUE} if the * IME is not visible or the user moved the stack since the IME became visible. @@ -173,7 +170,7 @@ public class StackAnimationController extends */ private boolean mSpringToTouchOnNextMotionEvent = false; - /** Horizontal offset of bubbles in the stack. */ + /** Offset of bubbles in the stack (i.e. how much they overlap). */ private float mStackOffset; /** Offset between stack y and animation y for bubble swap. */ private float mSwapAnimationOffset; @@ -305,10 +302,7 @@ public class StackAnimationController extends if (mLayout == null || !isStackPositionSet()) { return true; // Default to left, which is where it starts by default. } - - float stackCenter = mStackPosition.x + mBubbleSize / 2; - float screenCenter = mLayout.getWidth() / 2; - return stackCenter < screenCenter; + return mPositioner.isStackOnLeft(mStackPosition); } /** @@ -524,16 +518,6 @@ public class StackAnimationController extends removeEndActionForProperty(DynamicAnimation.TRANSLATION_Y); } - /** Save the current IME height so that we know where the stack bounds should be. */ - public void setImeHeight(int imeHeight) { - mImeHeight = imeHeight; - } - - /** Returns the current IME height that the stack is offset by. */ - public float getImeHeight() { - return mImeHeight; - } - /** * Animates the stack either away from the newly visible IME, or back to its original position * due to the IME going away. @@ -592,11 +576,14 @@ public class StackAnimationController extends */ public RectF getAllowableStackPositionRegion() { final RectF allowableRegion = new RectF(mPositioner.getAvailableRect()); + final int imeHeight = mPositioner.getImeHeight(); + final float bottomPadding = getBubbleCount() > 1 + ? mBubblePaddingTop + mStackOffset + : mBubblePaddingTop; allowableRegion.left -= mBubbleOffscreen; allowableRegion.top += mBubblePaddingTop; allowableRegion.right += mBubbleOffscreen - mBubbleSize; - allowableRegion.bottom -= mBubblePaddingTop + mBubbleSize - + (mImeHeight != UNSET ? mImeHeight + mBubblePaddingTop : 0f); + allowableRegion.bottom -= imeHeight + bottomPadding + mBubbleSize; return allowableRegion; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java index 3a7b534f3c17..ffda1f92ec90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java @@ -17,6 +17,7 @@ package com.android.wm.shell.common; import android.os.RemoteException; +import android.util.Slog; import android.view.IDisplayWindowRotationCallback; import android.view.IDisplayWindowRotationController; import android.view.IWindowManager; @@ -27,6 +28,7 @@ import androidx.annotation.BinderThread; import com.android.wm.shell.common.annotations.ShellMainThread; import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArrayList; /** * This module deals with display rotations coming from WM. When WM starts a rotation: after it has @@ -35,14 +37,14 @@ import java.util.ArrayList; * rotation. */ public class DisplayChangeController { + private static final String TAG = DisplayChangeController.class.getSimpleName(); private final ShellExecutor mMainExecutor; private final IWindowManager mWmService; private final IDisplayWindowRotationController mControllerImpl; - private final ArrayList<OnDisplayChangingListener> mRotationListener = - new ArrayList<>(); - private final ArrayList<OnDisplayChangingListener> mTmpListeners = new ArrayList<>(); + private final CopyOnWriteArrayList<OnDisplayChangingListener> mRotationListener = + new CopyOnWriteArrayList<>(); public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; @@ -59,34 +61,26 @@ public class DisplayChangeController { * Adds a display rotation controller. */ public void addRotationListener(OnDisplayChangingListener listener) { - synchronized (mRotationListener) { - mRotationListener.add(listener); - } + mRotationListener.add(listener); } /** * Removes a display rotation controller. */ public void removeRotationListener(OnDisplayChangingListener listener) { - synchronized (mRotationListener) { - mRotationListener.remove(listener); - } + mRotationListener.remove(listener); } private void onRotateDisplay(int displayId, final int fromRotation, final int toRotation, IDisplayWindowRotationCallback callback) { WindowContainerTransaction t = new WindowContainerTransaction(); - synchronized (mRotationListener) { - mTmpListeners.clear(); - // Make a local copy in case the handlers add/remove themselves. - mTmpListeners.addAll(mRotationListener); - } - for (OnDisplayChangingListener c : mTmpListeners) { + for (OnDisplayChangingListener c : mRotationListener) { c.onRotateDisplay(displayId, fromRotation, toRotation, t); } try { callback.continueRotateDisplay(toRotation, t); } catch (RemoteException e) { + Slog.e(TAG, "Failed to continue rotation", e); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index c63c67dec774..a1fb658ccb9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -26,6 +26,7 @@ import android.util.SparseArray; import android.view.Display; import android.view.IDisplayWindowListener; import android.view.IWindowManager; +import android.view.InsetsState; import androidx.annotation.BinderThread; @@ -52,14 +53,6 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); - /** - * Gets a display by id from DisplayManager. - */ - public Display getDisplay(int displayId) { - final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - return displayManager.getDisplay(displayId); - } - public DisplayController(Context context, IWindowManager wmService, ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; @@ -67,17 +60,31 @@ public class DisplayController { mWmService = wmService; mChangeController = new DisplayChangeController(mWmService, mainExecutor); mDisplayContainerListener = new DisplayWindowListenerImpl(); + } + + /** + * Initializes the window listener. + */ + public void initialize() { try { int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener); for (int i = 0; i < displayIds.length; i++) { onDisplayAdded(displayIds[i]); } } catch (RemoteException e) { - throw new RuntimeException("Unable to register hierarchy listener"); + throw new RuntimeException("Unable to register display controller"); } } /** + * Gets a display by id from DisplayManager. + */ + public Display getDisplay(int displayId) { + final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); + return displayManager.getDisplay(displayId); + } + + /** * Gets the DisplayLayout associated with a display. */ public @Nullable DisplayLayout getDisplayLayout(int displayId) { @@ -94,6 +101,16 @@ public class DisplayController { } /** + * Updates the insets for a given display. + */ + public void updateDisplayInsets(int displayId, InsetsState state) { + final DisplayRecord r = mDisplays.get(displayId); + if (r != null) { + r.setInsets(state); + } + } + + /** * Add a display window-container listener. It will get notified whenever a display's * configuration changes or when displays are added/removed from the WM hierarchy. */ @@ -137,17 +154,18 @@ public class DisplayController { if (mDisplays.get(displayId) != null) { return; } - Display display = getDisplay(displayId); + final Display display = getDisplay(displayId); if (display == null) { // It's likely that the display is private to some app and thus not // accessible by system-ui. return; } - DisplayRecord record = new DisplayRecord(); - record.mDisplayId = displayId; - record.mContext = (displayId == Display.DEFAULT_DISPLAY) ? mContext + + final Context context = (displayId == Display.DEFAULT_DISPLAY) + ? mContext : mContext.createDisplayContext(display); - record.mDisplayLayout = new DisplayLayout(record.mContext, display); + final DisplayRecord record = new DisplayRecord(displayId); + record.setDisplayLayout(context, new DisplayLayout(context, display)); mDisplays.put(displayId, record); for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { mDisplayChangedListeners.get(i).onDisplayAdded(displayId); @@ -157,24 +175,23 @@ public class DisplayController { private void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { synchronized (mDisplays) { - DisplayRecord dr = mDisplays.get(displayId); + final DisplayRecord dr = mDisplays.get(displayId); if (dr == null) { Slog.w(TAG, "Skipping Display Configuration change on non-added" + " display."); return; } - Display display = getDisplay(displayId); + final Display display = getDisplay(displayId); if (display == null) { Slog.w(TAG, "Skipping Display Configuration change on invalid" + " display. It may have been removed."); return; } - Context perDisplayContext = mContext; - if (displayId != Display.DEFAULT_DISPLAY) { - perDisplayContext = mContext.createDisplayContext(display); - } - dr.mContext = perDisplayContext.createConfigurationContext(newConfig); - dr.mDisplayLayout = new DisplayLayout(dr.mContext, display); + final Context perDisplayContext = (displayId == Display.DEFAULT_DISPLAY) + ? mContext + : mContext.createDisplayContext(display); + final Context context = perDisplayContext.createConfigurationContext(newConfig); + dr.setDisplayLayout(context, new DisplayLayout(context, display)); for (int i = 0; i < mDisplayChangedListeners.size(); ++i) { mDisplayChangedListeners.get(i).onDisplayConfigurationChanged( displayId, newConfig); @@ -222,9 +239,25 @@ public class DisplayController { } private static class DisplayRecord { - int mDisplayId; - Context mContext; - DisplayLayout mDisplayLayout; + private int mDisplayId; + private Context mContext; + private DisplayLayout mDisplayLayout; + private InsetsState mInsetsState = new InsetsState(); + + private DisplayRecord(int displayId) { + mDisplayId = displayId; + } + + private void setDisplayLayout(Context context, DisplayLayout displayLayout) { + mContext = context; + mDisplayLayout = displayLayout; + mDisplayLayout.setInsets(mContext.getResources(), mInsetsState); + } + + private void setInsets(InsetsState state) { + mInsetsState = state; + mDisplayLayout.setInsets(mContext.getResources(), state); + } } @BinderThread 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 a7996f056785..a7052bc49699 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 @@ -33,6 +33,7 @@ import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; +import android.view.InsetsVisibilities; import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowInsets; @@ -68,14 +69,17 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged protected final Executor mMainExecutor; private final TransactionPool mTransactionPool; private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; private final SparseArray<PerDisplay> mImePerDisplay = new SparseArray<>(); private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>(); public DisplayImeController(IWindowManager wmService, DisplayController displayController, + DisplayInsetsController displayInsetsController, Executor mainExecutor, TransactionPool transactionPool) { mWmService = wmService; mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; mMainExecutor = mainExecutor; mTransactionPool = transactionPool; } @@ -109,11 +113,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onDisplayRemoved(int displayId) { - try { - mWmService.setDisplayWindowInsetsController(displayId, null); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to remove insets controller on display " + displayId); + PerDisplay pd = mImePerDisplay.get(displayId); + if (pd == null) { + return; } + pd.unregister(); mImePerDisplay.remove(displayId); } @@ -195,11 +199,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */ - public class PerDisplay { + public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { final int mDisplayId; final InsetsState mInsetsState = new InsetsState(); - protected final DisplayWindowInsetsControllerImpl mInsetsControllerImpl = - new DisplayWindowInsetsControllerImpl(); + final InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities(); InsetsSourceControl mImeSourceControl = null; int mAnimationDirection = DIRECTION_NONE; ValueAnimator mAnimation = null; @@ -214,14 +217,15 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } public void register() { - try { - mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl); - } catch (RemoteException e) { - Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId); - } + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); } - protected void insetsChanged(InsetsState insetsState) { + public void unregister() { + mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { if (mInsetsState.equals(insetsState)) { return; } @@ -239,8 +243,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + @Override @VisibleForTesting - protected void insetsControlChanged(InsetsState insetsState, + public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) { insetsChanged(insetsState); InsetsSourceControl imeSourceControl = null; @@ -279,9 +284,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged if (!mImeShowing) { removeImeSurface(); } - } - if (mImeSourceControl != null) { - mImeSourceControl.release(SurfaceControl::release); + if (mImeSourceControl != null) { + mImeSourceControl.release(SurfaceControl::release); + } } mImeSourceControl = imeSourceControl; } @@ -301,7 +306,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } - protected void showInsets(int types, boolean fromIme) { + @Override + public void showInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { return; } @@ -309,8 +315,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged startAnimation(true /* show */, false /* forceRestart */); } - - protected void hideInsets(int types, boolean fromIme) { + @Override + public void hideInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { return; } @@ -318,6 +324,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged startAnimation(false /* show */, false /* forceRestart */); } + @Override public void topFocusedWindowChanged(String packageName) { // Do nothing } @@ -327,8 +334,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged */ private void setVisibleDirectly(boolean visible) { mInsetsState.getSource(InsetsState.ITYPE_IME).setVisible(visible); + mRequestedVisibilities.setVisibility(InsetsState.ITYPE_IME, visible); try { - mWmService.modifyDisplayWindowInsets(mDisplayId, mInsetsState); + mWmService.updateDisplayWindowRequestedVisibilities(mDisplayId, + mRequestedVisibilities); } catch (RemoteException e) { } } @@ -489,47 +498,6 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged dispatchVisibilityChanged(mDisplayId, isShowing); } } - - @VisibleForTesting - @BinderThread - public class DisplayWindowInsetsControllerImpl - extends IDisplayWindowInsetsController.Stub { - @Override - public void topFocusedWindowChanged(String packageName) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.topFocusedWindowChanged(packageName); - }); - } - - @Override - public void insetsChanged(InsetsState insetsState) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.insetsChanged(insetsState); - }); - } - - @Override - public void insetsControlChanged(InsetsState insetsState, - InsetsSourceControl[] activeControls) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.insetsControlChanged(insetsState, activeControls); - }); - } - - @Override - public void showInsets(int types, boolean fromIme) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.showInsets(types, fromIme); - }); - } - - @Override - public void hideInsets(int types, boolean fromIme) throws RemoteException { - mMainExecutor.execute(() -> { - PerDisplay.this.hideInsets(types, fromIme); - }); - } - } } void removeImeSurface() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java new file mode 100644 index 000000000000..565f1481233c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.os.RemoteException; +import android.util.Slog; +import android.util.SparseArray; +import android.view.IDisplayWindowInsetsController; +import android.view.IWindowManager; +import android.view.InsetsSourceControl; +import android.view.InsetsState; + +import androidx.annotation.BinderThread; + +import com.android.wm.shell.common.annotations.ShellMainThread; + +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Manages insets from the core. + */ +public class DisplayInsetsController implements DisplayController.OnDisplaysChangedListener { + private static final String TAG = "DisplayInsetsController"; + + private final IWindowManager mWmService; + private final ShellExecutor mMainExecutor; + private final DisplayController mDisplayController; + private final SparseArray<PerDisplay> mInsetsPerDisplay = new SparseArray<>(); + private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners = + new SparseArray<>(); + + public DisplayInsetsController(IWindowManager wmService, DisplayController displayController, + ShellExecutor mainExecutor) { + mWmService = wmService; + mDisplayController = displayController; + mMainExecutor = mainExecutor; + } + + /** + * Starts listening for insets for each display. + **/ + public void initialize() { + mDisplayController.addDisplayWindowListener(this); + } + + /** + * Adds a callback to listen for insets changes for a particular display. Note that the + * listener will not be updated with the existing state of the insets on that display. + */ + public void addInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId); + if (listeners == null) { + listeners = new CopyOnWriteArrayList<>(); + mListeners.put(displayId, listeners); + } + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + /** + * Removes a callback listening for insets changes from a particular display. + */ + public void removeInsetsChangedListener(int displayId, OnInsetsChangedListener listener) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(displayId); + if (listeners == null) { + return; + } + listeners.remove(listener); + } + + @Override + public void onDisplayAdded(int displayId) { + PerDisplay pd = new PerDisplay(displayId); + pd.register(); + mInsetsPerDisplay.put(displayId, pd); + } + + @Override + public void onDisplayRemoved(int displayId) { + PerDisplay pd = mInsetsPerDisplay.get(displayId); + if (pd == null) { + return; + } + pd.unregister(); + mInsetsPerDisplay.remove(displayId); + } + + /** + * An implementation of {@link IDisplayWindowInsetsController} for a given display id. + **/ + public class PerDisplay { + private final int mDisplayId; + private final DisplayWindowInsetsControllerImpl mInsetsControllerImpl = + new DisplayWindowInsetsControllerImpl(); + + public PerDisplay(int displayId) { + mDisplayId = displayId; + } + + public void register() { + try { + mWmService.setDisplayWindowInsetsController(mDisplayId, mInsetsControllerImpl); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to set insets controller on display " + mDisplayId); + } + } + + public void unregister() { + try { + mWmService.setDisplayWindowInsetsController(mDisplayId, null); + } catch (RemoteException e) { + Slog.w(TAG, "Unable to remove insets controller on display " + mDisplayId); + } + } + + private void insetsChanged(InsetsState insetsState) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + mDisplayController.updateDisplayInsets(mDisplayId, insetsState); + for (OnInsetsChangedListener listener : listeners) { + listener.insetsChanged(insetsState); + } + } + + private void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.insetsControlChanged(insetsState, activeControls); + } + } + + private void showInsets(int types, boolean fromIme) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.showInsets(types, fromIme); + } + } + + private void hideInsets(int types, boolean fromIme) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.hideInsets(types, fromIme); + } + } + + private void topFocusedWindowChanged(String packageName) { + CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId); + if (listeners == null) { + return; + } + for (OnInsetsChangedListener listener : listeners) { + listener.topFocusedWindowChanged(packageName); + } + } + + @BinderThread + private class DisplayWindowInsetsControllerImpl + extends IDisplayWindowInsetsController.Stub { + @Override + public void topFocusedWindowChanged(String packageName) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.topFocusedWindowChanged(packageName); + }); + } + + @Override + public void insetsChanged(InsetsState insetsState) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.insetsChanged(insetsState); + }); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.insetsControlChanged(insetsState, activeControls); + }); + } + + @Override + public void showInsets(int types, boolean fromIme) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.showInsets(types, fromIme); + }); + } + + @Override + public void hideInsets(int types, boolean fromIme) throws RemoteException { + mMainExecutor.execute(() -> { + PerDisplay.this.hideInsets(types, fromIme); + }); + } + } + } + + /** + * Gets notified whenever the insets change. + * + * @see IDisplayWindowInsetsController + */ + @ShellMainThread + public interface OnInsetsChangedListener { + /** + * Called when top focused window changes to determine whether or not to take over insets + * control. Won't be called if config_remoteInsetsControllerControlsSystemBars is false. + * @param packageName: Passes the top package name + */ + default void topFocusedWindowChanged(String packageName) {} + + /** + * Called when the window insets configuration has changed. + */ + default void insetsChanged(InsetsState insetsState) {} + + /** + * Called when this window retrieved control over a specified set of insets sources. + */ + default void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) {} + + /** + * Called when a set of insets source window should be shown by policy. + * + * @param types internal insets types (WindowInsets.Type.InsetsType) to show + * @param fromIme true if this request originated from IME (InputMethodService). + */ + default void showInsets(int types, boolean fromIme) {} + + /** + * Called when a set of insets source window should be hidden by policy. + * + * @param types internal insets types (WindowInsets.Type.InsetsType) to hide + * @param fromIme true if this request originated from IME (InputMethodService). + */ + default void hideInsets(int types, boolean fromIme) {} + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java index a568c28dacf1..6f4e22fa8a04 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayLayout.java @@ -25,6 +25,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON import static android.util.RotationUtils.rotateBounds; import static android.util.RotationUtils.rotateInsets; import static android.view.Display.FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; +import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; @@ -44,9 +45,14 @@ import android.view.Display; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.Gravity; +import android.view.InsetsSource; +import android.view.InsetsState; import android.view.Surface; +import androidx.annotation.VisibleForTesting; + import com.android.internal.R; +import com.android.internal.policy.SystemBarUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -82,6 +88,10 @@ public class DisplayLayout { private boolean mHasNavigationBar = false; private boolean mHasStatusBar = false; private int mNavBarFrameHeight = 0; + private boolean mAllowSeamlessRotationDespiteNavBarMoving = false; + private boolean mNavigationBarCanMove = false; + private boolean mReverseDefaultRotation = false; + private InsetsState mInsetsState = new InsetsState(); /** * Different from {@link #equals(Object)}, this method compares the basic geometry properties @@ -111,14 +121,20 @@ public class DisplayLayout { && Objects.equals(mStableInsets, other.mStableInsets) && mHasNavigationBar == other.mHasNavigationBar && mHasStatusBar == other.mHasStatusBar - && mNavBarFrameHeight == other.mNavBarFrameHeight; + && mAllowSeamlessRotationDespiteNavBarMoving + == other.mAllowSeamlessRotationDespiteNavBarMoving + && mNavigationBarCanMove == other.mNavigationBarCanMove + && mReverseDefaultRotation == other.mReverseDefaultRotation + && mNavBarFrameHeight == other.mNavBarFrameHeight + && Objects.equals(mInsetsState, other.mInsetsState); } @Override public int hashCode() { return Objects.hash(mUiMode, mWidth, mHeight, mCutout, mRotation, mDensityDpi, mNonDecorInsets, mStableInsets, mHasNavigationBar, mHasStatusBar, - mNavBarFrameHeight); + mNavBarFrameHeight, mAllowSeamlessRotationDespiteNavBarMoving, + mNavigationBarCanMove, mReverseDefaultRotation, mInsetsState); } /** @@ -163,9 +179,13 @@ public class DisplayLayout { mDensityDpi = dl.mDensityDpi; mHasNavigationBar = dl.mHasNavigationBar; mHasStatusBar = dl.mHasStatusBar; + mAllowSeamlessRotationDespiteNavBarMoving = dl.mAllowSeamlessRotationDespiteNavBarMoving; + mNavigationBarCanMove = dl.mNavigationBarCanMove; + mReverseDefaultRotation = dl.mReverseDefaultRotation; mNavBarFrameHeight = dl.mNavBarFrameHeight; mNonDecorInsets.set(dl.mNonDecorInsets); mStableInsets.set(dl.mStableInsets); + mInsetsState.set(dl.mInsetsState, true /* copySources */); } private void init(DisplayInfo info, Resources res, boolean hasNavigationBar, @@ -178,15 +198,28 @@ public class DisplayLayout { mDensityDpi = info.logicalDensityDpi; mHasNavigationBar = hasNavigationBar; mHasStatusBar = hasStatusBar; + mAllowSeamlessRotationDespiteNavBarMoving = res.getBoolean( + R.bool.config_allowSeamlessRotationDespiteNavBarMoving); + mNavigationBarCanMove = res.getBoolean(R.bool.config_navBarCanMove); + mReverseDefaultRotation = res.getBoolean(R.bool.config_reverseDefaultRotation); recalcInsets(res); } - private void recalcInsets(Resources res) { - computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mUiMode, mNonDecorInsets, - mHasNavigationBar); + /** + * Updates the current insets. + */ + public void setInsets(Resources res, InsetsState state) { + mInsetsState = state; + recalcInsets(res); + } + + @VisibleForTesting + void recalcInsets(Resources res) { + computeNonDecorInsets(res, mRotation, mWidth, mHeight, mCutout, mInsetsState, mUiMode, + mNonDecorInsets, mHasNavigationBar); mStableInsets.set(mNonDecorInsets); if (mHasStatusBar) { - convertNonDecorInsetsToStableInsets(res, mStableInsets, mWidth, mHeight, mHasStatusBar); + convertNonDecorInsetsToStableInsets(res, mStableInsets, mCutout, mHasStatusBar); } mNavBarFrameHeight = getNavigationBarFrameHeight(res, mWidth > mHeight); } @@ -257,11 +290,33 @@ public class DisplayLayout { return mWidth > mHeight; } - /** Get the navbar frame height (used by ime). */ + /** Get the navbar frame (or window) height (used by ime). */ public int navBarFrameHeight() { return mNavBarFrameHeight; } + /** @return whether we can seamlessly rotate even if nav-bar can change sides. */ + public boolean allowSeamlessRotationDespiteNavBarMoving() { + return mAllowSeamlessRotationDespiteNavBarMoving; + } + + /** @return whether the navigation bar will change sides during rotation. */ + public boolean navigationBarCanMove() { + return mNavigationBarCanMove; + } + + /** @return the rotation that would make the physical display "upside down". */ + public int getUpsideDownRotation() { + boolean displayHardwareIsLandscape = mWidth > mHeight; + if ((mRotation % 2) != 0) { + displayHardwareIsLandscape = !displayHardwareIsLandscape; + } + if (displayHardwareIsLandscape) { + return mReverseDefaultRotation ? Surface.ROTATION_270 : Surface.ROTATION_90; + } + return Surface.ROTATION_180; + } + /** Gets the orientation of this layout */ public int getOrientation() { return (mWidth > mHeight) ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; @@ -284,12 +339,12 @@ public class DisplayLayout { /** * Calculates the stable insets if we already have the non-decor insets. */ - private static void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets, - int displayWidth, int displayHeight, boolean hasStatusBar) { + private void convertNonDecorInsetsToStableInsets(Resources res, Rect inOutInsets, + DisplayCutout cutout, boolean hasStatusBar) { if (!hasStatusBar) { return; } - int statusBarHeight = getStatusBarHeight(displayWidth > displayHeight, res); + int statusBarHeight = SystemBarUtils.getStatusBarHeight(res, cutout); inOutInsets.top = Math.max(inOutInsets.top, statusBarHeight); } @@ -304,21 +359,29 @@ public class DisplayLayout { * @param outInsets the insets to return */ static void computeNonDecorInsets(Resources res, int displayRotation, int displayWidth, - int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, - boolean hasNavigationBar) { + int displayHeight, DisplayCutout displayCutout, InsetsState insetsState, int uiMode, + Rect outInsets, boolean hasNavigationBar) { outInsets.setEmpty(); // Only navigation bar if (hasNavigationBar) { + final InsetsSource extraNavBar = insetsState.getSource(ITYPE_EXTRA_NAVIGATION_BAR); + final boolean hasExtraNav = extraNavBar != null && extraNavBar.isVisible(); int position = navigationBarPosition(res, displayWidth, displayHeight, displayRotation); int navBarSize = getNavigationBarSize(res, position, displayWidth > displayHeight, uiMode); if (position == NAV_BAR_BOTTOM) { - outInsets.bottom = navBarSize; + outInsets.bottom = hasExtraNav + ? Math.max(navBarSize, extraNavBar.getFrame().height()) + : navBarSize; } else if (position == NAV_BAR_RIGHT) { - outInsets.right = navBarSize; + outInsets.right = hasExtraNav + ? Math.max(navBarSize, extraNavBar.getFrame().width()) + : navBarSize; } else if (position == NAV_BAR_LEFT) { - outInsets.left = navBarSize; + outInsets.left = hasExtraNav + ? Math.max(navBarSize, extraNavBar.getFrame().width()) + : navBarSize; } } @@ -330,35 +393,6 @@ public class DisplayLayout { } } - /** - * Calculates the stable insets without running a layout. - * - * @param displayRotation the current display rotation - * @param displayWidth the current display width - * @param displayHeight the current display height - * @param displayCutout the current display cutout - * @param outInsets the insets to return - */ - static void computeStableInsets(Resources res, int displayRotation, int displayWidth, - int displayHeight, DisplayCutout displayCutout, int uiMode, Rect outInsets, - boolean hasNavigationBar, boolean hasStatusBar) { - outInsets.setEmpty(); - - // Navigation bar and status bar. - computeNonDecorInsets(res, displayRotation, displayWidth, displayHeight, displayCutout, - uiMode, outInsets, hasNavigationBar); - convertNonDecorInsetsToStableInsets(res, outInsets, displayWidth, displayHeight, - hasStatusBar); - } - - /** Retrieve the statusbar height from resources. */ - static int getStatusBarHeight(boolean landscape, Resources res) { - return landscape ? res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height_landscape) - : res.getDimensionPixelSize( - com.android.internal.R.dimen.status_bar_height_portrait); - } - /** Calculate the DisplayCutout for a particular display size/rotation. */ public static DisplayCutout calculateDisplayCutoutForRotation( DisplayCutout cutout, int rotation, int displayWidth, int displayHeight) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java new file mode 100644 index 000000000000..b77ac8a2b951 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SingleInstanceRemoteListener.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.os.IBinder; +import android.os.IInterface; +import android.os.RemoteException; +import android.util.Slog; + +import androidx.annotation.BinderThread; + +import java.util.function.Consumer; + +/** + * Manages the lifecycle of a single instance of a remote listener, including the clean up if the + * remote process dies. All calls on this class should happen on the main shell thread. + * + * @param <C> The controller (must be RemoteCallable) + * @param <L> The remote listener interface type + */ +public class SingleInstanceRemoteListener<C extends RemoteCallable, L extends IInterface> { + private static final String TAG = SingleInstanceRemoteListener.class.getSimpleName(); + + /** + * Simple callable interface that throws a remote exception. + */ + public interface RemoteCall<L> { + void accept(L l) throws RemoteException; + } + + private final C mCallableController; + private final Consumer<C> mOnRegisterCallback; + private final Consumer<C> mOnUnregisterCallback; + + L mListener; + + private final IBinder.DeathRecipient mListenerDeathRecipient = + new IBinder.DeathRecipient() { + @Override + @BinderThread + public void binderDied() { + final C callableController = mCallableController; + mCallableController.getRemoteCallExecutor().execute(() -> { + mListener = null; + mOnUnregisterCallback.accept(callableController); + }); + } + }; + + /** + * @param onRegisterCallback Callback when register() is called (same thread) + * @param onUnregisterCallback Callback when unregister() is called (same thread as unregister() + * or the callableController.getRemoteCallbackExecutor() thread) + */ + public SingleInstanceRemoteListener(C callableController, + Consumer<C> onRegisterCallback, + Consumer<C> onUnregisterCallback) { + mCallableController = callableController; + mOnRegisterCallback = onRegisterCallback; + mOnUnregisterCallback = onUnregisterCallback; + } + + /** + * Registers this listener, storing a reference to it and calls the provided method in the + * constructor. + */ + public void register(L listener) { + if (mListener != null) { + mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, 0 /* flags */); + } + if (listener != null) { + try { + listener.asBinder().linkToDeath(mListenerDeathRecipient, 0 /* flags */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death"); + return; + } + } + mListener = listener; + mOnRegisterCallback.accept(mCallableController); + } + + /** + * Unregisters this listener, removing all references to it and calls the provided method in the + * constructor. + */ + public void unregister() { + if (mListener != null) { + mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, 0 /* flags */); + } + mListener = null; + mOnUnregisterCallback.accept(mCallableController); + } + + /** + * Safely wraps a call to the remote listener. + */ + public void call(RemoteCall<L> handler) { + if (mListener == null) { + Slog.e(TAG, "Failed remote call on null listener"); + return; + } + try { + handler.accept(mListener); + } catch (RemoteException e) { + Slog.e(TAG, "Failed remote call", e); + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java index 55c5125f0a00..4b138e43bc3f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java @@ -23,16 +23,22 @@ import android.view.SurfaceSession; * Helpers for handling surface. */ public class SurfaceUtils { - /** Creates a dim layer above indicated host surface. */ + /** Creates a dim layer above host surface. */ public static SurfaceControl makeDimLayer(SurfaceControl.Transaction t, SurfaceControl host, String name, SurfaceSession surfaceSession) { - SurfaceControl dimLayer = new SurfaceControl.Builder(surfaceSession) + final SurfaceControl dimLayer = makeColorLayer(host, name, surfaceSession); + t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f}); + return dimLayer; + } + + /** Creates a color layer for host surface. */ + public static SurfaceControl makeColorLayer(SurfaceControl host, String name, + SurfaceSession surfaceSession) { + return new SurfaceControl.Builder(surfaceSession) .setParent(host) .setColorLayer() .setName(name) - .setCallsite("SurfaceUtils.makeDimLayer") + .setCallsite("SurfaceUtils.makeColorLayer") .build(); - t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f}); - return dimLayer; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java index 33beab5ee3f1..4c0281dcc517 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java @@ -18,13 +18,15 @@ package com.android.wm.shell.common; import android.annotation.BinderThread; import android.annotation.NonNull; +import android.os.RemoteException; import android.util.Slog; import android.view.SurfaceControl; +import android.view.WindowManager; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; import android.window.WindowOrganizer; -import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.transition.LegacyTransitions; import java.util.ArrayList; @@ -66,6 +68,10 @@ public final class SyncTransactionQueue { * Queues a sync transaction to be sent serially to WM. */ public void queue(WindowContainerTransaction wct) { + if (wct.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty"); + return; + } SyncCallback cb = new SyncCallback(wct); synchronized (mQueue) { if (DEBUG) Slog.d(TAG, "Queueing up " + wct); @@ -77,11 +83,34 @@ public final class SyncTransactionQueue { } /** + * Queues a legacy transition to be sent serially to WM + */ + public void queue(LegacyTransitions.ILegacyTransition transition, + @WindowManager.TransitionType int type, WindowContainerTransaction wct) { + if (wct.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Skip queue due to transaction change is empty"); + return; + } + SyncCallback cb = new SyncCallback(transition, type, wct); + synchronized (mQueue) { + if (DEBUG) Slog.d(TAG, "Queueing up legacy transition " + wct); + mQueue.add(cb); + if (mQueue.size() == 1) { + cb.send(); + } + } + } + + /** * Queues a sync transaction only if there are already sync transaction(s) queued or in flight. * Otherwise just returns without queueing. * @return {@code true} if queued, {@code false} if not. */ public boolean queueIfWaiting(WindowContainerTransaction wct) { + if (wct.isEmpty()) { + if (DEBUG) Slog.d(TAG, "Skip queueIfWaiting due to transaction change is empty"); + return false; + } synchronized (mQueue) { if (mQueue.isEmpty()) { if (DEBUG) Slog.d(TAG, "Nothing in queue, so skip queueing up " + wct); @@ -118,12 +147,12 @@ public final class SyncTransactionQueue { // Synchronized on mQueue private void onTransactionReceived(@NonNull SurfaceControl.Transaction t) { if (DEBUG) Slog.d(TAG, " Running " + mRunnables.size() + " sync runnables"); - for (int i = 0, n = mRunnables.size(); i < n; ++i) { + final int n = mRunnables.size(); + for (int i = 0; i < n; ++i) { mRunnables.get(i).runWithTransaction(t); } - mRunnables.clear(); - t.apply(); - t.close(); + // More runnables may have been added, so only remove the ones that ran. + mRunnables.subList(0, n).clear(); } /** Task to run with transaction. */ @@ -135,20 +164,38 @@ public final class SyncTransactionQueue { private class SyncCallback extends WindowContainerTransactionCallback { int mId = -1; final WindowContainerTransaction mWCT; + final LegacyTransitions.LegacyTransition mLegacyTransition; SyncCallback(WindowContainerTransaction wct) { mWCT = wct; + mLegacyTransition = null; + } + + SyncCallback(LegacyTransitions.ILegacyTransition legacyTransition, + @WindowManager.TransitionType int type, WindowContainerTransaction wct) { + mWCT = wct; + mLegacyTransition = new LegacyTransitions.LegacyTransition(type, legacyTransition); } // Must be sychronized on mQueue void send() { + if (mInFlight == this) { + // This was probably queued up and sent during a sync runnable of the last callback. + // Don't queue it again. + return; + } if (mInFlight != null) { throw new IllegalStateException("Sync Transactions must be serialized. In Flight: " + mInFlight.mId + " - " + mInFlight.mWCT); } mInFlight = this; if (DEBUG) Slog.d(TAG, "Sending sync transaction: " + mWCT); - mId = new WindowOrganizer().applySyncTransaction(mWCT, this); + if (mLegacyTransition != null) { + mId = new WindowOrganizer().startLegacyTransition(mLegacyTransition.getType(), + mLegacyTransition.getAdapter(), this, mWCT); + } else { + mId = new WindowOrganizer().applySyncTransaction(mWCT, this); + } if (DEBUG) Slog.d(TAG, " Sent sync transaction. Got id=" + mId); mMainExecutor.executeDelayed(mOnReplyTimeout, REPLY_TIMEOUT); } @@ -169,6 +216,16 @@ public final class SyncTransactionQueue { if (DEBUG) Slog.d(TAG, "onTransactionReady id=" + mId); mQueue.remove(this); onTransactionReceived(t); + if (mLegacyTransition != null) { + try { + mLegacyTransition.getSyncCallback().onTransactionReady(mId, t); + } catch (RemoteException e) { + Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e); + } + } else { + t.apply(); + t.close(); + } if (!mQueue.isEmpty()) { mQueue.get(0).send(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java new file mode 100644 index 000000000000..9ac7a12bc509 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** + * Annotates a method or qualifies a provider that runs on the main-thread of the process using + * this library. + */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface ExternalMainThread { +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java index 218bf47e24aa..c76937de6669 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerHandleView.java @@ -70,7 +70,8 @@ public class DividerHandleView extends View { private final Paint mPaint = new Paint(); private final int mWidth; private final int mHeight; - private final int mCircleDiameter; + private final int mTouchingWidth; + private final int mTouchingHeight; private int mCurrentWidth; private int mCurrentHeight; private AnimatorSet mAnimator; @@ -80,11 +81,12 @@ public class DividerHandleView extends View { super(context, attrs); mPaint.setColor(getResources().getColor(R.color.docked_divider_handle, null)); mPaint.setAntiAlias(true); - mWidth = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_width); - mHeight = getResources().getDimensionPixelSize(R.dimen.docked_divider_handle_height); + mWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_width); + mHeight = getResources().getDimensionPixelSize(R.dimen.split_divider_handle_height); mCurrentWidth = mWidth; mCurrentHeight = mHeight; - mCircleDiameter = (mWidth + mHeight) / 3; + mTouchingWidth = mWidth > mHeight ? mWidth / 2 : mWidth; + mTouchingHeight = mHeight > mWidth ? mHeight / 2 : mHeight; } /** Sets touching state for this handle view. */ @@ -98,16 +100,16 @@ public class DividerHandleView extends View { } if (!animate) { if (touching) { - mCurrentWidth = mCircleDiameter; - mCurrentHeight = mCircleDiameter; + mCurrentWidth = mTouchingWidth; + mCurrentHeight = mTouchingHeight; } else { mCurrentWidth = mWidth; mCurrentHeight = mHeight; } invalidate(); } else { - animateToTarget(touching ? mCircleDiameter : mWidth, - touching ? mCircleDiameter : mHeight, touching); + animateToTarget(touching ? mTouchingWidth : mWidth, + touching ? mTouchingHeight : mHeight, touching); } mTouching = touching; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java new file mode 100644 index 000000000000..364bb651d55d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerRoundedCorner.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; +import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; +import static android.view.RoundedCorner.POSITION_TOP_LEFT; +import static android.view.RoundedCorner.POSITION_TOP_RIGHT; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.util.AttributeSet; +import android.view.RoundedCorner; +import android.view.View; + +import androidx.annotation.Nullable; + +import com.android.wm.shell.R; + +/** + * Draws inverted rounded corners beside divider bar to keep splitting tasks cropped with proper + * rounded corners. + */ +public class DividerRoundedCorner extends View { + private final int mDividerWidth; + private final Paint mDividerBarBackground; + private final Point mStartPos = new Point(); + private InvertedRoundedCornerDrawInfo mTopLeftCorner; + private InvertedRoundedCornerDrawInfo mTopRightCorner; + private InvertedRoundedCornerDrawInfo mBottomLeftCorner; + private InvertedRoundedCornerDrawInfo mBottomRightCorner; + + public DividerRoundedCorner(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mDividerWidth = getResources().getDimensionPixelSize(R.dimen.split_divider_bar_width); + mDividerBarBackground = new Paint(); + mDividerBarBackground.setColor( + getResources().getColor(R.color.split_divider_background, null)); + mDividerBarBackground.setFlags(Paint.ANTI_ALIAS_FLAG); + mDividerBarBackground.setStyle(Paint.Style.FILL); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mTopLeftCorner = new InvertedRoundedCornerDrawInfo(POSITION_TOP_LEFT); + mTopRightCorner = new InvertedRoundedCornerDrawInfo(POSITION_TOP_RIGHT); + mBottomLeftCorner = new InvertedRoundedCornerDrawInfo(POSITION_BOTTOM_LEFT); + mBottomRightCorner = new InvertedRoundedCornerDrawInfo(POSITION_BOTTOM_RIGHT); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.save(); + + mTopLeftCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mTopLeftCorner.mPath, mDividerBarBackground); + + canvas.translate(-mStartPos.x, -mStartPos.y); + mTopRightCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mTopRightCorner.mPath, mDividerBarBackground); + + canvas.translate(-mStartPos.x, -mStartPos.y); + mBottomLeftCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mBottomLeftCorner.mPath, mDividerBarBackground); + + canvas.translate(-mStartPos.x, -mStartPos.y); + mBottomRightCorner.calculateStartPos(mStartPos); + canvas.translate(mStartPos.x, mStartPos.y); + canvas.drawPath(mBottomRightCorner.mPath, mDividerBarBackground); + + canvas.restore(); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + private boolean isLandscape() { + return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; + } + + /** + * Holds draw information of the inverted rounded corner at a specific position. + * + * @see {@link com.android.launcher3.taskbar.TaskbarDragLayer} + */ + private class InvertedRoundedCornerDrawInfo { + @RoundedCorner.Position + private final int mCornerPosition; + + private final int mRadius; + + private final Path mPath = new Path(); + + InvertedRoundedCornerDrawInfo(@RoundedCorner.Position int cornerPosition) { + mCornerPosition = cornerPosition; + + final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(cornerPosition); + mRadius = roundedCorner == null ? 0 : roundedCorner.getRadius(); + + // Starts with a filled square, and then subtracting out a circle from the appropriate + // corner. + final Path square = new Path(); + square.addRect(0, 0, mRadius, mRadius, Path.Direction.CW); + final Path circle = new Path(); + circle.addCircle( + isLeftCorner() ? mRadius : 0 /* x */, + isTopCorner() ? mRadius : 0 /* y */, + mRadius, Path.Direction.CW); + mPath.op(square, circle, Path.Op.DIFFERENCE); + } + + private void calculateStartPos(Point outPos) { + if (isLandscape()) { + // Place left corner at the right side of the divider bar. + outPos.x = isLeftCorner() + ? getWidth() / 2 + mDividerWidth / 2 + : getWidth() / 2 - mDividerWidth / 2 - mRadius; + outPos.y = isTopCorner() ? 0 : getHeight() - mRadius; + } else { + outPos.x = isLeftCorner() ? 0 : getWidth() - mRadius; + // Place top corner at the bottom of the divider bar. + outPos.y = isTopCorner() + ? getHeight() / 2 + mDividerWidth / 2 + : getHeight() / 2 - mDividerWidth / 2 - mRadius; + } + } + + private boolean isLeftCorner() { + return mCornerPosition == POSITION_TOP_LEFT || mCornerPosition == POSITION_BOTTOM_LEFT; + } + + private boolean isTopCorner() { + return mCornerPosition == POSITION_TOP_LEFT || mCornerPosition == POSITION_TOP_RIGHT; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index cba019a11b28..4b125b118ceb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -19,14 +19,23 @@ package com.android.wm.shell.common.split; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; import android.content.Context; +import android.graphics.Rect; import android.util.AttributeSet; +import android.util.Property; import android.view.GestureDetector; +import android.view.InsetsController; +import android.view.InsetsSource; +import android.view.InsetsState; import android.view.MotionEvent; import android.view.SurfaceControlViewHost; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; @@ -44,9 +53,13 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { public static final long TOUCH_ANIMATION_DURATION = 150; public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; + /** The task bar expanded height. Used to determine whether to insets divider bounds or not. */ + private float mExpandedTaskBarHeight; + private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); private SplitLayout mSplitLayout; + private SplitWindowManager mSplitWindowManager; private SurfaceControlViewHost mViewHost; private DividerHandleView mHandle; private View mBackground; @@ -57,6 +70,44 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private int mStartPos; private GestureDetector mDoubleTapDetector; private boolean mInteractive; + private boolean mSetTouchRegion = true; + + /** + * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with + * insets. + */ + private final Rect mDividerBounds = new Rect(); + private final Rect mTempRect = new Rect(); + private FrameLayout mDividerBar; + + + static final Property<DividerView, Integer> DIVIDER_HEIGHT_PROPERTY = + new Property<DividerView, Integer>(Integer.class, "height") { + @Override + public Integer get(DividerView object) { + return object.mDividerBar.getLayoutParams().height; + } + + @Override + public void set(DividerView object, Integer value) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) + object.mDividerBar.getLayoutParams(); + lp.height = value; + object.mDividerBar.setLayoutParams(lp); + } + }; + + private AnimatorListenerAdapter mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mSetTouchRegion = true; + } + + @Override + public void onAnimationCancel(Animator animation) { + mSetTouchRegion = true; + } + }; public DividerView(@NonNull Context context) { super(context); @@ -79,16 +130,50 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { /** Sets up essential dependencies of the divider bar. */ public void setup( SplitLayout layout, - SurfaceControlViewHost viewHost) { + SplitWindowManager splitWindowManager, + SurfaceControlViewHost viewHost, + InsetsState insetsState) { mSplitLayout = layout; + mSplitWindowManager = splitWindowManager; mViewHost = viewHost; + mDividerBounds.set(layout.getDividerBounds()); + onInsetsChanged(insetsState, false /* animate */); + } + + void onInsetsChanged(InsetsState insetsState, boolean animate) { + mTempRect.set(mSplitLayout.getDividerBounds()); + final InsetsSource taskBarInsetsSource = + insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + // Only insets the divider bar with task bar when it's expanded so that the rounded corners + // will be drawn against task bar. + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mTempRect.inset(taskBarInsetsSource.calculateVisibleInsets(mTempRect)); + } + + if (!mTempRect.equals(mDividerBounds)) { + if (animate) { + ObjectAnimator animator = ObjectAnimator.ofInt(this, + DIVIDER_HEIGHT_PROPERTY, mDividerBounds.height(), mTempRect.height()); + animator.setInterpolator(InsetsController.RESIZE_INTERPOLATOR); + animator.setDuration(InsetsController.ANIMATION_DURATION_RESIZE); + animator.addListener(mAnimatorListener); + animator.start(); + } else { + DIVIDER_HEIGHT_PROPERTY.set(this, mTempRect.height()); + mSetTouchRegion = true; + } + mDividerBounds.set(mTempRect); + } } @Override protected void onFinishInflate() { super.onFinishInflate(); + mDividerBar = findViewById(R.id.divider_bar); mHandle = findViewById(R.id.docked_divider_handle); mBackground = findViewById(R.id.docked_divider_background); + mExpandedTaskBarHeight = getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); mTouchElevation = getResources().getDimensionPixelSize( R.dimen.docked_stack_divider_lift_elevation); mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); @@ -97,6 +182,17 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mSetTouchRegion) { + mTempRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), + mHandle.getBottom()); + mSplitWindowManager.setTouchRegion(mTempRect); + mSetTouchRegion = false; + } + } + + @Override public boolean onTouch(View v, MotionEvent event) { if (mSplitLayout == null || !mInteractive) { return false; @@ -106,10 +202,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { return true; } + // Convert to use screen-based coordinates to prevent lost track of motion events while + // moving divider bar and calculating dragging velocity. + event.setLocation(event.getRawX(), event.getRawY()); final int action = event.getAction() & MotionEvent.ACTION_MASK; final boolean isLandscape = isLandscape(); - // Using raw xy to prevent lost track of motion events while moving divider bar. - final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY(); + final int touchPos = (int) (isLandscape ? event.getX() : event.getY()); switch (action) { case MotionEvent.ACTION_DOWN: mVelocityTracker = VelocityTracker.obtain(); @@ -153,16 +251,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private void setTouching() { setSlippery(false); mHandle.setTouching(true, true); - if (isLandscape()) { - mBackground.animate().scaleX(1.4f); - } else { - mBackground.animate().scaleY(1.4f); - } - mBackground.animate() - .setInterpolator(Interpolators.TOUCH_RESPONSE) - .setDuration(TOUCH_ANIMATION_DURATION) - .translationZ(mTouchElevation) - .start(); // Lift handle as well so it doesn't get behind the background, even though it doesn't // cast shadow. mHandle.animate() @@ -175,13 +263,6 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { private void releaseTouching() { setSlippery(true); mHandle.setTouching(false, true); - mBackground.animate() - .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) - .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) - .translationZ(0) - .scaleX(1f) - .scaleY(1f) - .start(); mHandle.animate() .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java new file mode 100644 index 000000000000..36e55bae18c3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Binder; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.R; +import com.android.wm.shell.common.SurfaceUtils; + +/** + * Handles split decor like showing resizing hint for a specific split. + */ +public class SplitDecorManager extends WindowlessWindowManager { + private static final String TAG = SplitDecorManager.class.getSimpleName(); + private static final String RESIZING_BACKGROUND_SURFACE_NAME = "ResizingBackground"; + + private final IconProvider mIconProvider; + private final SurfaceSession mSurfaceSession; + + private Drawable mIcon; + private ImageView mResizingIconView; + private SurfaceControlViewHost mViewHost; + private SurfaceControl mHostLeash; + private SurfaceControl mIconLeash; + private SurfaceControl mBackgroundLeash; + + public SplitDecorManager(Configuration configuration, IconProvider iconProvider, + SurfaceSession surfaceSession) { + super(configuration, null /* rootSurface */, null /* hostInputToken */); + mIconProvider = iconProvider; + mSurfaceSession = surfaceSession; + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName(TAG) + .setHidden(true) + .setParent(mHostLeash) + .setCallsite("SplitDecorManager#attachToParentSurface"); + mIconLeash = builder.build(); + b.setParent(mIconLeash); + } + + /** Inflates split decor surface on the root surface. */ + public void inflate(Context context, SurfaceControl rootLeash, Rect rootBounds) { + if (mIconLeash != null && mViewHost != null) { + return; + } + + context = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, + null /* options */); + mHostLeash = rootLeash; + mViewHost = new SurfaceControlViewHost(context, context.getDisplay(), this); + + final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(context) + .inflate(R.layout.split_decor, null); + mResizingIconView = rootLayout.findViewById(R.id.split_resizing_icon); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); + lp.width = rootBounds.width(); + lp.height = rootBounds.height(); + lp.token = new Binder(); + lp.setTitle(TAG); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports + // TRUSTED_OVERLAY for windowless window without input channel. + mViewHost.setView(rootLayout, lp); + } + + /** Releases the surfaces for split decor. */ + public void release(SurfaceControl.Transaction t) { + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + if (mIconLeash != null) { + t.remove(mIconLeash); + mIconLeash = null; + } + if (mBackgroundLeash != null) { + t.remove(mBackgroundLeash); + mBackgroundLeash = null; + } + mHostLeash = null; + mIcon = null; + mResizingIconView = null; + } + + /** Showing resizing hint. */ + public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds, + SurfaceControl.Transaction t) { + if (mResizingIconView == null) { + return; + } + + if (mBackgroundLeash == null) { + mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash, + RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession); + t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask)) + .setLayer(mBackgroundLeash, SPLIT_DIVIDER_LAYER - 1) + .show(mBackgroundLeash); + } + + if (mIcon == null && resizingTask.topActivityInfo != null) { + // TODO: add fade-in animation. + mIcon = mIconProvider.getIcon(resizingTask.topActivityInfo); + mResizingIconView.setImageDrawable(mIcon); + mResizingIconView.setVisibility(View.VISIBLE); + + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = mIcon.getIntrinsicWidth(); + lp.height = mIcon.getIntrinsicHeight(); + mViewHost.relayout(lp); + t.show(mIconLeash).setLayer(mIconLeash, SPLIT_DIVIDER_LAYER); + } + + t.setPosition(mIconLeash, + newBounds.width() / 2 - mIcon.getIntrinsicWidth() / 2, + newBounds.height() / 2 - mIcon.getIntrinsicWidth() / 2); + } + + /** Stops showing resizing hint. */ + public void onResized(Rect newBounds, SurfaceControl.Transaction t) { + if (mResizingIconView == null) { + return; + } + + if (mBackgroundLeash != null) { + t.remove(mBackgroundLeash); + mBackgroundLeash = null; + } + + if (mIcon != null) { + mResizingIconView.setVisibility(View.GONE); + mResizingIconView.setImageDrawable(null); + t.hide(mIconLeash); + mIcon = null; + } + } + + private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 5b158d2063ba..ba343cb12085 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -16,21 +16,37 @@ package com.android.wm.shell.common.split; +import static android.content.res.Configuration.SCREEN_HEIGHT_DP_UNDEFINED; +import static android.content.res.Configuration.SCREEN_WIDTH_DP_UNDEFINED; +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; import static android.view.WindowManager.DOCKED_TOP; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END; import static com.android.internal.policy.DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START; +import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.annotation.IntDef; +import android.annotation.NonNull; import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.Rect; +import android.view.Display; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.RoundedCorner; import android.view.SurfaceControl; import android.view.WindowInsets; import android.view.WindowManager; @@ -39,84 +55,95 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.DividerSnapAlgorithm; +import com.android.internal.policy.DockedDividerUtils; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; + +import java.io.PrintWriter; /** * Records and handles layout of splits. Helps to calculate proper bounds when configuration or * divide position changes. */ -public final class SplitLayout { - /** - * Split position isn't specified normally meaning to use what ever it is currently set to. - */ - public static final int SPLIT_POSITION_UNDEFINED = -1; - - /** - * Specifies that a split is positioned at the top half of the screen if - * in portrait mode or at the left half of the screen if in landscape mode. - */ - public static final int SPLIT_POSITION_TOP_OR_LEFT = 0; - - /** - * Specifies that a split is positioned at the bottom half of the screen if - * in portrait mode or at the right half of the screen if in landscape mode. - */ - public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1; - - @IntDef(prefix = {"SPLIT_POSITION_"}, value = { - SPLIT_POSITION_UNDEFINED, - SPLIT_POSITION_TOP_OR_LEFT, - SPLIT_POSITION_BOTTOM_OR_RIGHT - }) - public @interface SplitPosition { - } +public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener { private final int mDividerWindowWidth; private final int mDividerInsets; private final int mDividerSize; + private final Rect mTempRect = new Rect(); private final Rect mRootBounds = new Rect(); private final Rect mDividerBounds = new Rect(); private final Rect mBounds1 = new Rect(); private final Rect mBounds2 = new Rect(); + private final Rect mWinBounds1 = new Rect(); + private final Rect mWinBounds2 = new Rect(); private final SplitLayoutHandler mSplitLayoutHandler; private final SplitWindowManager mSplitWindowManager; private final DisplayImeController mDisplayImeController; private final ImePositionProcessor mImePositionProcessor; + private final DismissingEffectPolicy mDismissingEffectPolicy; private final ShellTaskOrganizer mTaskOrganizer; + private final InsetsState mInsetsState = new InsetsState(); private Context mContext; private DividerSnapAlgorithm mDividerSnapAlgorithm; + private WindowContainerToken mWinToken1; + private WindowContainerToken mWinToken2; private int mDividePosition; private boolean mInitialized = false; + private int mOrientation; + private int mRotation; public SplitLayout(String windowName, Context context, Configuration configuration, SplitLayoutHandler splitLayoutHandler, SplitWindowManager.ParentContainerCallbacks parentContainerCallbacks, - DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer) { + DisplayImeController displayImeController, ShellTaskOrganizer taskOrganizer, + boolean applyDismissingParallax) { mContext = context.createConfigurationContext(configuration); + mOrientation = configuration.orientation; + mRotation = configuration.windowConfiguration.getRotation(); mSplitLayoutHandler = splitLayoutHandler; mDisplayImeController = displayImeController; - mSplitWindowManager = new SplitWindowManager( - windowName, mContext, configuration, parentContainerCallbacks); + mSplitWindowManager = new SplitWindowManager(windowName, mContext, configuration, + parentContainerCallbacks); mTaskOrganizer = taskOrganizer; mImePositionProcessor = new ImePositionProcessor(mContext.getDisplayId()); + mDismissingEffectPolicy = new DismissingEffectPolicy(applyDismissingParallax); final Resources resources = context.getResources(); - mDividerWindowWidth = resources.getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_thickness); - mDividerInsets = resources.getDimensionPixelSize( - com.android.internal.R.dimen.docked_stack_divider_insets); - mDividerSize = mDividerWindowWidth - mDividerInsets * 2; + mDividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width); + mDividerInsets = getDividerInsets(resources, context.getDisplay()); + mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; mRootBounds.set(configuration.windowConfiguration.getBounds()); mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); resetDividerPosition(); } + private int getDividerInsets(Resources resources, Display display) { + final int dividerInset = resources.getDimensionPixelSize( + com.android.internal.R.dimen.docked_stack_divider_insets); + + int radius = 0; + RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); + radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; + corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT); + radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; + corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT); + radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; + corner = display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT); + radius = corner != null ? Math.max(radius, corner.getRadius()) : radius; + + return Math.max(dividerInset, radius); + } + /** Gets bounds of the primary split. */ public Rect getBounds1() { return new Rect(mBounds1); @@ -142,35 +169,71 @@ public final class SplitLayout { return mDividePosition; } + /** + * Returns the divider position as a fraction from 0 to 1. + */ + public float getDividerPositionAsFraction() { + return Math.min(1f, Math.max(0f, isLandscape() + ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right + : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); + } + /** Applies new configuration, returns {@code false} if there's no effect to the layout. */ public boolean updateConfiguration(Configuration configuration) { + boolean affectsLayout = false; + + // Update the split bounds when necessary. Besides root bounds changed, split bounds need to + // be updated when the rotation changed to cover the case that users rotated the screen 180 + // degrees. + // Make sure to render the divider bar with proper resources that matching the screen + // orientation. + final int rotation = configuration.windowConfiguration.getRotation(); final Rect rootBounds = configuration.windowConfiguration.getBounds(); - if (mRootBounds.equals(rootBounds)) { + final int orientation = configuration.orientation; + + if (mOrientation == orientation + && rotation == mRotation + && mRootBounds.equals(rootBounds)) { return false; } mContext = mContext.createConfigurationContext(configuration); mSplitWindowManager.setConfiguration(configuration); + mOrientation = orientation; + mTempRect.set(mRootBounds); mRootBounds.set(rootBounds); + mRotation = rotation; mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); - resetDividerPosition(); + initDividerPosition(mTempRect); - // Don't inflate divider bar if it is not initialized. - if (!mInitialized) { - return false; + if (mInitialized) { + release(); + init(); } - release(); - init(); return true; } + private void initDividerPosition(Rect oldBounds) { + final float snapRatio = (float) mDividePosition + / (float) (isLandscape(oldBounds) ? oldBounds.width() : oldBounds.height()); + // Estimate position by previous ratio. + final float length = + (float) (isLandscape() ? mRootBounds.width() : mRootBounds.height()); + final int estimatePosition = (int) (length * snapRatio); + // Init divider position by estimated position using current bounds snap algorithm. + mDividePosition = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( + estimatePosition).position; + updateBounds(mDividePosition); + } + /** Updates recording bounds of divider window and both of the splits. */ private void updateBounds(int position) { mDividerBounds.set(mRootBounds); mBounds1.set(mRootBounds); mBounds2.set(mRootBounds); - if (isLandscape(mRootBounds)) { + final boolean isLandscape = isLandscape(mRootBounds); + if (isLandscape) { position += mRootBounds.left; mDividerBounds.left = position - mDividerInsets; mDividerBounds.right = mDividerBounds.left + mDividerWindowWidth; @@ -183,13 +246,16 @@ public final class SplitLayout { mBounds1.bottom = position; mBounds2.top = mBounds1.bottom + mDividerSize; } + DockedDividerUtils.sanitizeStackBounds(mBounds1, true /** topLeft */); + DockedDividerUtils.sanitizeStackBounds(mBounds2, false /** topLeft */); + mDismissingEffectPolicy.applyDividerPosition(position, isLandscape); } /** Inflates {@link DividerView} on the root surface. */ public void init() { if (mInitialized) return; mInitialized = true; - mSplitWindowManager.init(this); + mSplitWindowManager.init(this, mInsetsState); mDisplayImeController.addPositionProcessor(mImePositionProcessor); } @@ -202,27 +268,56 @@ public final class SplitLayout { mImePositionProcessor.reset(); } + @Override + public void insetsChanged(InsetsState insetsState) { + mInsetsState.set(insetsState); + if (!mInitialized) { + return; + } + mSplitWindowManager.onInsetsChanged(insetsState); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + if (!mInsetsState.equals(insetsState)) { + insetsChanged(insetsState); + } + } + /** * Updates bounds with the passing position. Usually used to update recording bounds while * performing animation or dragging divider bar to resize the splits. */ void updateDivideBounds(int position) { updateBounds(position); - mSplitWindowManager.setResizingSplits(true); - mSplitLayoutHandler.onBoundsChanging(this); + mSplitLayoutHandler.onLayoutSizeChanging(this); } void setDividePosition(int position) { mDividePosition = position; updateBounds(mDividePosition); - mSplitLayoutHandler.onBoundsChanged(this); - mSplitWindowManager.setResizingSplits(false); + mSplitLayoutHandler.onLayoutSizeChanged(this); + } + + /** Sets divide position base on the ratio within root bounds. */ + public void setDivideRatio(float ratio) { + final int position = isLandscape() + ? mRootBounds.left + (int) (mRootBounds.width() * ratio) + : mRootBounds.top + (int) (mRootBounds.height() * ratio); + DividerSnapAlgorithm.SnapTarget snapTarget = + mDividerSnapAlgorithm.calculateNonDismissingSnapTarget(position); + setDividePosition(snapTarget.position); } /** Resets divider position. */ public void resetDividerPosition() { mDividePosition = mDividerSnapAlgorithm.getMiddleTarget().position; updateBounds(mDividePosition); + mWinToken1 = null; + mWinToken2 = null; + mWinBounds1.setEmpty(); + mWinBounds2.setEmpty(); } /** @@ -232,15 +327,15 @@ public final class SplitLayout { public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) { switch (snapTarget.flag) { case FLAG_DISMISS_START: - mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */); - mSplitWindowManager.setResizingSplits(false); + flingDividePosition(currentPosition, snapTarget.position, + () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */)); break; case FLAG_DISMISS_END: - mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */); - mSplitWindowManager.setResizingSplits(false); + flingDividePosition(currentPosition, snapTarget.position, + () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */)); break; default: - flingDividePosition(currentPosition, snapTarget.position); + flingDividePosition(currentPosition, snapTarget.position, null); break; } } @@ -270,8 +365,13 @@ public final class SplitLayout { isLandscape ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } - private void flingDividePosition(int from, int to) { - if (from == to) return; + @VisibleForTesting + void flingDividePosition(int from, int to, @Nullable Runnable flingFinishedCallback) { + if (from == to) { + // No animation run, still callback to stop resizing. + mSplitLayoutHandler.onLayoutSizeChanged(this); + return; + } ValueAnimator animator = ValueAnimator .ofInt(from, to) .setDuration(250); @@ -282,6 +382,9 @@ public final class SplitLayout { @Override public void onAnimationEnd(Animator animation) { setDividePosition(to); + if (flingFinishedCallback != null) { + flingFinishedCallback.run(); + } } @Override @@ -296,42 +399,119 @@ public final class SplitLayout { return context.getSystemService(WindowManager.class) .getMaximumWindowMetrics() .getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars() - | WindowInsets.Type.statusBars() - | WindowInsets.Type.displayCutout()).toRect(); + .getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()) + .toRect(); } private static boolean isLandscape(Rect bounds) { return bounds.width() > bounds.height(); } + /** Reverse the split position. */ + @SplitPosition + public static int reversePosition(@SplitPosition int position) { + switch (position) { + case SPLIT_POSITION_TOP_OR_LEFT: + return SPLIT_POSITION_BOTTOM_OR_RIGHT; + case SPLIT_POSITION_BOTTOM_OR_RIGHT: + return SPLIT_POSITION_TOP_OR_LEFT; + default: + return SPLIT_POSITION_UNDEFINED; + } + } + + /** + * Return if this layout is landscape. + */ + public boolean isLandscape() { + return isLandscape(mRootBounds); + } + /** Apply recorded surface layout to the {@link SurfaceControl.Transaction}. */ public void applySurfaceChanges(SurfaceControl.Transaction t, SurfaceControl leash1, SurfaceControl leash2, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { - final Rect dividerBounds = mImePositionProcessor.adjustForIme(mDividerBounds); - final Rect bounds1 = mImePositionProcessor.adjustForIme(mBounds1); - final Rect bounds2 = mImePositionProcessor.adjustForIme(mBounds2); final SurfaceControl dividerLeash = getDividerLeash(); if (dividerLeash != null) { - t.setPosition(dividerLeash, dividerBounds.left, dividerBounds.top) - // Resets layer of divider bar to make sure it is always on top. - .setLayer(dividerLeash, Integer.MAX_VALUE); + t.setPosition(dividerLeash, mDividerBounds.left, mDividerBounds.top); + // Resets layer of divider bar to make sure it is always on top. + t.setLayer(dividerLeash, SPLIT_DIVIDER_LAYER); + } + t.setPosition(leash1, mBounds1.left, mBounds1.top) + .setWindowCrop(leash1, mBounds1.width(), mBounds1.height()); + t.setPosition(leash2, mBounds2.left, mBounds2.top) + .setWindowCrop(leash2, mBounds2.width(), mBounds2.height()); + + if (mImePositionProcessor.adjustSurfaceLayoutForIme( + t, dividerLeash, leash1, leash2, dimLayer1, dimLayer2)) { + return; } - t.setPosition(leash1, bounds1.left, bounds1.top) - .setWindowCrop(leash1, bounds1.width(), bounds1.height()); - - t.setPosition(leash2, bounds2.left, bounds2.top) - .setWindowCrop(leash2, bounds2.width(), bounds2.height()); - - mImePositionProcessor.applySurfaceDimValues(t, dimLayer1, dimLayer2); + mDismissingEffectPolicy.adjustDismissingSurface(t, leash1, leash2, dimLayer1, dimLayer2); } /** Apply recorded task layout to the {@link WindowContainerTransaction}. */ public void applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { - wct.setBounds(task1.token, mImePositionProcessor.adjustForIme(mBounds1)) - .setBounds(task2.token, mImePositionProcessor.adjustForIme(mBounds2)); + if (mImePositionProcessor.applyTaskLayoutForIme(wct, task1.token, task2.token)) { + return; + } + + if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { + wct.setBounds(task1.token, mBounds1); + mWinBounds1.set(mBounds1); + mWinToken1 = task1.token; + } + if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { + wct.setBounds(task2.token, mBounds2); + mWinBounds2.set(mBounds2); + mWinToken2 = task2.token; + } + } + + /** + * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And + * restore shifted configuration bounds if it's no longer shifted. + */ + public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, + ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { + if (offsetX == 0 && offsetY == 0) { + wct.setBounds(taskInfo1.token, mBounds1); + wct.setAppBounds(taskInfo1.token, null); + wct.setScreenSizeDp(taskInfo1.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + + wct.setBounds(taskInfo2.token, mBounds2); + wct.setAppBounds(taskInfo2.token, null); + wct.setScreenSizeDp(taskInfo2.token, + SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); + } else { + mTempRect.set(taskInfo1.configuration.windowConfiguration.getBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setBounds(taskInfo1.token, mTempRect); + mTempRect.set(taskInfo1.configuration.windowConfiguration.getAppBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setAppBounds(taskInfo1.token, mTempRect); + wct.setScreenSizeDp(taskInfo1.token, + taskInfo1.configuration.screenWidthDp, + taskInfo1.configuration.screenHeightDp); + + mTempRect.set(taskInfo2.configuration.windowConfiguration.getBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setBounds(taskInfo2.token, mTempRect); + mTempRect.set(taskInfo2.configuration.windowConfiguration.getAppBounds()); + mTempRect.offset(offsetX, offsetY); + wct.setAppBounds(taskInfo2.token, mTempRect); + wct.setScreenSizeDp(taskInfo2.token, + taskInfo2.configuration.screenWidthDp, + taskInfo2.configuration.screenHeightDp); + } + } + + /** Dumps the current split bounds recorded in this layout. */ + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + "bounds1=" + mBounds1.toShortString()); + pw.println(prefix + "dividerBounds=" + mDividerBounds.toShortString()); + pw.println(prefix + "bounds2=" + mBounds2.toShortString()); } /** Handles layout change event. */ @@ -340,11 +520,43 @@ public final class SplitLayout { /** Calls when dismissing split. */ void onSnappedToDismiss(boolean snappedToEnd); - /** Calls when the bounds is changing due to animation or dragging divider bar. */ - void onBoundsChanging(SplitLayout layout); + /** + * Calls when resizing the split bounds. + * + * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, + * SurfaceControl, SurfaceControl) + */ + void onLayoutSizeChanging(SplitLayout layout); + + /** + * Calls when finish resizing the split bounds. + * + * @see #applyTaskChanges(WindowContainerTransaction, ActivityManager.RunningTaskInfo, + * ActivityManager.RunningTaskInfo) + * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, + * SurfaceControl, SurfaceControl) + */ + void onLayoutSizeChanged(SplitLayout layout); - /** Calls when the target bounds changed. */ - void onBoundsChanged(SplitLayout layout); + /** + * Calls when re-positioning the split bounds. Like moving split bounds while showing IME + * panel. + * + * @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl, + * SurfaceControl, SurfaceControl) + */ + void onLayoutPositionChanging(SplitLayout layout); + + /** + * Notifies the target offset for shifting layout. So layout handler can shift configuration + * bounds correspondingly to make sure client apps won't get configuration changed or + * relaunched. If the layout is no longer shifted, layout handler should restore shifted + * configuration bounds. + * + * @see #applyLayoutOffsetTarget(WindowContainerTransaction, int, int, + * ActivityManager.RunningTaskInfo, ActivityManager.RunningTaskInfo) + */ + void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout); /** Calls when user double tapped on the divider bar. */ default void onDoubleTappedDivider() { @@ -355,6 +567,115 @@ public final class SplitLayout { int getSplitItemPosition(WindowContainerToken token); } + /** + * Calculates and applies proper dismissing parallax offset and dimming value to hint users + * dismissing gesture. + */ + private class DismissingEffectPolicy { + /** Indicates whether to offset splitting bounds to hint dismissing progress or not. */ + private final boolean mApplyParallax; + + // The current dismissing side. + int mDismissingSide = DOCKED_INVALID; + + // The parallax offset to hint the dismissing side and progress. + final Point mDismissingParallaxOffset = new Point(); + + // The dimming value to hint the dismissing side and progress. + float mDismissingDimValue = 0.0f; + + DismissingEffectPolicy(boolean applyDismissingParallax) { + mApplyParallax = applyDismissingParallax; + } + + /** + * Applies a parallax to the task to hint dismissing progress. + * + * @param position the split position to apply dismissing parallax effect + * @param isLandscape indicates whether it's splitting horizontally or vertically + */ + void applyDividerPosition(int position, boolean isLandscape) { + mDismissingSide = DOCKED_INVALID; + mDismissingParallaxOffset.set(0, 0); + mDismissingDimValue = 0; + + int totalDismissingDistance = 0; + if (position < mDividerSnapAlgorithm.getFirstSplitTarget().position) { + mDismissingSide = isLandscape ? DOCKED_LEFT : DOCKED_TOP; + totalDismissingDistance = mDividerSnapAlgorithm.getDismissStartTarget().position + - mDividerSnapAlgorithm.getFirstSplitTarget().position; + } else if (position > mDividerSnapAlgorithm.getLastSplitTarget().position) { + mDismissingSide = isLandscape ? DOCKED_RIGHT : DOCKED_BOTTOM; + totalDismissingDistance = mDividerSnapAlgorithm.getLastSplitTarget().position + - mDividerSnapAlgorithm.getDismissEndTarget().position; + } + + if (mDismissingSide != DOCKED_INVALID) { + float fraction = Math.max(0, + Math.min(mDividerSnapAlgorithm.calculateDismissingFraction(position), 1f)); + mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); + fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); + if (isLandscape) { + mDismissingParallaxOffset.x = (int) (fraction * totalDismissingDistance); + } else { + mDismissingParallaxOffset.y = (int) (fraction * totalDismissingDistance); + } + } + } + + /** + * @return for a specified {@code fraction}, this returns an adjusted value that simulates a + * slowing down parallax effect + */ + private float calculateParallaxDismissingFraction(float fraction, int dockSide) { + float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; + + // Less parallax at the top, just because. + if (dockSide == WindowManager.DOCKED_TOP) { + result /= 2f; + } + return result; + } + + /** Applies parallax offset and dimming value to the root surface at the dismissing side. */ + boolean adjustDismissingSurface(SurfaceControl.Transaction t, + SurfaceControl leash1, SurfaceControl leash2, + SurfaceControl dimLayer1, SurfaceControl dimLayer2) { + SurfaceControl targetLeash, targetDimLayer; + switch (mDismissingSide) { + case DOCKED_TOP: + case DOCKED_LEFT: + targetLeash = leash1; + targetDimLayer = dimLayer1; + mTempRect.set(mBounds1); + break; + case DOCKED_BOTTOM: + case DOCKED_RIGHT: + targetLeash = leash2; + targetDimLayer = dimLayer2; + mTempRect.set(mBounds2); + break; + case DOCKED_INVALID: + default: + t.setAlpha(dimLayer1, 0).hide(dimLayer1); + t.setAlpha(dimLayer2, 0).hide(dimLayer2); + return false; + } + + if (mApplyParallax) { + t.setPosition(targetLeash, + mTempRect.left + mDismissingParallaxOffset.x, + mTempRect.top + mDismissingParallaxOffset.y); + // Transform the screen-based split bounds to surface-based crop bounds. + mTempRect.offsetTo(-mDismissingParallaxOffset.x, -mDismissingParallaxOffset.y); + t.setWindowCrop(targetLeash, mTempRect); + } + t.setAlpha(targetDimLayer, mDismissingDimValue) + .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); + return true; + } + } + /** Records IME top offset changes and updates SplitLayout correspondingly. */ private class ImePositionProcessor implements DisplayImeController.ImePositionProcessor { /** @@ -409,6 +730,18 @@ public final class SplitLayout { && !isFloating && !isLandscape(mRootBounds) && showing; mTargetYOffset = needOffset ? getTargetYOffset() : 0; + if (mTargetYOffset != mLastYOffset) { + // Freeze the configuration size with offset to prevent app get a configuration + // changed or relaunch. This is required to make sure client apps will calculate + // insets properly after layout shifted. + if (mTargetYOffset == 0) { + mSplitLayoutHandler.setLayoutOffsetTarget(0, 0, SplitLayout.this); + } else { + mSplitLayoutHandler.setLayoutOffsetTarget(0, mTargetYOffset - mLastYOffset, + SplitLayout.this); + } + } + // Make {@link DividerView} non-interactive while IME showing in split mode. Listen to // ImePositionProcessor#onImeVisibilityChanged directly in DividerView is not enough // because DividerView won't receive onImeVisibilityChanged callback after it being @@ -423,7 +756,7 @@ public final class SplitLayout { public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) { if (displayId != mDisplayId) return; onProgress(getProgress(imeTop)); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @Override @@ -431,7 +764,7 @@ public final class SplitLayout { SurfaceControl.Transaction t) { if (displayId != mDisplayId || cancel) return; onProgress(1.0f); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } @Override @@ -441,7 +774,7 @@ public final class SplitLayout { if (!controlling && mImeShown) { reset(); mSplitWindowManager.setInteractive(true); - mSplitLayoutHandler.onBoundsChanging(SplitLayout.this); + mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this); } } @@ -473,24 +806,66 @@ public final class SplitLayout { return start + (end - start) * progress; } - private void reset() { + void reset() { mImeShown = false; mYOffsetForIme = mLastYOffset = mTargetYOffset = 0; mDimValue1 = mLastDim1 = mTargetDim1 = 0.0f; mDimValue2 = mLastDim2 = mTargetDim2 = 0.0f; } - /* Adjust bounds with IME offset. */ - private Rect adjustForIme(Rect bounds) { - final Rect temp = new Rect(bounds); - if (mYOffsetForIme != 0) temp.offset(0, mYOffsetForIme); - return temp; + /** + * Applies adjusted task layout for showing IME. + * + * @return {@code false} if there's no need to adjust, otherwise {@code true} + */ + boolean applyTaskLayoutForIme(WindowContainerTransaction wct, + WindowContainerToken token1, WindowContainerToken token2) { + if (mYOffsetForIme == 0) return false; + + mTempRect.set(mBounds1); + mTempRect.offset(0, mYOffsetForIme); + wct.setBounds(token1, mTempRect); + + mTempRect.set(mBounds2); + mTempRect.offset(0, mYOffsetForIme); + wct.setBounds(token2, mTempRect); + + return true; } - private void applySurfaceDimValues(SurfaceControl.Transaction t, SurfaceControl dimLayer1, - SurfaceControl dimLayer2) { - t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); - t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); + /** + * Adjusts surface layout while showing IME. + * + * @return {@code false} if there's no need to adjust, otherwise {@code true} + */ + boolean adjustSurfaceLayoutForIme(SurfaceControl.Transaction t, + SurfaceControl dividerLeash, SurfaceControl leash1, SurfaceControl leash2, + SurfaceControl dimLayer1, SurfaceControl dimLayer2) { + final boolean showDim = mDimValue1 > 0.001f || mDimValue2 > 0.001f; + boolean adjusted = false; + if (mYOffsetForIme != 0) { + if (dividerLeash != null) { + mTempRect.set(mDividerBounds); + mTempRect.offset(0, mYOffsetForIme); + t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); + } + + mTempRect.set(mBounds1); + mTempRect.offset(0, mYOffsetForIme); + t.setPosition(leash1, mTempRect.left, mTempRect.top); + + mTempRect.set(mBounds2); + mTempRect.offset(0, mYOffsetForIme); + t.setPosition(leash2, mTempRect.left, mTempRect.top); + adjusted = true; + } + + if (showDim) { + t.setAlpha(dimLayer1, mDimValue1).setVisibility(dimLayer1, mDimValue1 > 0.001f); + t.setAlpha(dimLayer2, mDimValue2).setVisibility(dimLayer2, mDimValue2 > 0.001f); + adjusted = true; + } + return adjusted; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java new file mode 100644 index 000000000000..9b614875119b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.common.split; + +import android.annotation.IntDef; + +/** Helper utility class of methods and constants that are available to be imported in Launcher. */ +public class SplitScreenConstants { + + /** + * Split position isn't specified normally meaning to use what ever it is currently set to. + */ + public static final int SPLIT_POSITION_UNDEFINED = -1; + + /** + * Specifies that a split is positioned at the top half of the screen if + * in portrait mode or at the left half of the screen if in landscape mode. + */ + public static final int SPLIT_POSITION_TOP_OR_LEFT = 0; + + /** + * Specifies that a split is positioned at the bottom half of the screen if + * in portrait mode or at the right half of the screen if in landscape mode. + */ + public static final int SPLIT_POSITION_BOTTOM_OR_RIGHT = 1; + + @IntDef(prefix = {"SPLIT_POSITION_"}, value = { + SPLIT_POSITION_UNDEFINED, + SPLIT_POSITION_TOP_OR_LEFT, + SPLIT_POSITION_BOTTOM_OR_RIGHT + }) + public @interface SplitPosition { + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java index 0cea0efc0057..4903f9d46dc7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java @@ -25,17 +25,14 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; -import android.app.ActivityTaskManager; import android.content.Context; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Region; import android.os.Binder; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Slog; import android.view.IWindow; +import android.view.InsetsState; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; @@ -43,6 +40,7 @@ import android.view.SurfaceSession; import android.view.WindowManager; import android.view.WindowlessWindowManager; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.wm.shell.R; @@ -58,11 +56,11 @@ public final class SplitWindowManager extends WindowlessWindowManager { private Context mContext; private SurfaceControlViewHost mViewHost; private SurfaceControl mLeash; - private boolean mResizingSplits; private DividerView mDividerView; public interface ParentContainerCallbacks { void attachToParentSurface(SurfaceControl.Builder b); + void onLeashReady(SurfaceControl leash); } public SplitWindowManager(String windowName, Context context, Configuration config, @@ -73,9 +71,10 @@ public final class SplitWindowManager extends WindowlessWindowManager { mWindowName = windowName; } - @Override - public void setTouchRegion(IBinder window, Region region) { - super.setTouchRegion(window, region); + void setTouchRegion(@NonNull Rect region) { + if (mViewHost != null) { + setTouchRegion(mViewHost.getWindowToken().asBinder(), new Region(region)); + } } @Override @@ -95,15 +94,16 @@ public final class SplitWindowManager extends WindowlessWindowManager { final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) .setContainerLayer() .setName(TAG) - .setHidden(false) + .setHidden(true) .setCallsite("SplitWindowManager#attachToParentSurface"); mParentContainerCallbacks.attachToParentSurface(builder); mLeash = builder.build(); + mParentContainerCallbacks.onLeashReady(mLeash); b.setParent(mLeash); } /** Inflates {@link DividerView} on to the root surface. */ - void init(SplitLayout splitLayout) { + void init(SplitLayout splitLayout, InsetsState insetsState) { if (mDividerView != null || mViewHost != null) { throw new UnsupportedOperationException( "Try to inflate divider view again without release first"); @@ -123,7 +123,7 @@ public final class SplitWindowManager extends WindowlessWindowManager { lp.setTitle(mWindowName); lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; mViewHost.setView(mDividerView, lp); - mDividerView.setup(splitLayout, mViewHost); + mDividerView.setup(splitLayout, this, mViewHost, insetsState); } /** @@ -151,16 +151,6 @@ public final class SplitWindowManager extends WindowlessWindowManager { mDividerView.setInteractive(interactive); } - void setResizingSplits(boolean resizing) { - if (resizing == mResizingSplits) return; - try { - ActivityTaskManager.getService().setSplitScreenResizing(resizing); - mResizingSplits = resizing; - } catch (RemoteException e) { - Slog.w(TAG, "Error calling setSplitScreenResizing", e); - } - } - /** * Gets {@link SurfaceControl} of the surface holding divider view. @return {@code null} if not * feasible. @@ -169,4 +159,10 @@ public final class SplitWindowManager extends WindowlessWindowManager { SurfaceControl getSurfaceControl() { return mLeash; } + + void onInsetsChanged(InsetsState insetsState) { + if (mDividerView != null) { + mDividerView.onInsetsChanged(insetsState, true /* animate */); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java new file mode 100644 index 000000000000..99dbfe01964c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUI.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 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.compatui; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface to engage compat UI. + */ +@ExternalThread +public interface CompatUI { + /** + * Called when the keyguard occluded state changes. Removes all compat UIs if the + * keyguard is now occluded. + * @param occluded indicates if the keyguard is now occluded. + */ + void onKeyguardOccludedChanged(boolean occluded); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 1fc4d12def1f..e0b23873a980 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.sizecompatui; +package com.android.wm.shell.compatui; import android.annotation.Nullable; import android.content.Context; @@ -24,99 +24,135 @@ import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.view.Display; +import android.view.InsetsSourceControl; +import android.view.InsetsState; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.annotations.ExternalThread; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; /** * Controls to show/update restart-activity buttons on Tasks based on whether the foreground - * activities are in size compatibility mode. + * activities are in compatibility mode. */ -public class SizeCompatUIController implements DisplayController.OnDisplaysChangedListener, +public class CompatUIController implements OnDisplaysChangedListener, DisplayImeController.ImePositionProcessor { /** Callback for size compat UI interaction. */ - public interface SizeCompatUICallback { + public interface CompatUICallback { + /** Called when the size compat restart button appears. */ + void onSizeCompatRestartButtonAppeared(int taskId); /** Called when the size compat restart button is clicked. */ void onSizeCompatRestartButtonClicked(int taskId); } - private static final String TAG = "SizeCompatUIController"; + private static final String TAG = "CompatUIController"; /** Whether the IME is shown on display id. */ private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1); + /** {@link PerDisplayOnInsetsChangedListener} by display id. */ + private final SparseArray<PerDisplayOnInsetsChangedListener> mOnInsetsChangedListeners = + new SparseArray<>(0); + /** The showing UIs by task id. */ - private final SparseArray<SizeCompatUILayout> mActiveLayouts = new SparseArray<>(0); + private final SparseArray<CompatUIWindowManager> mActiveLayouts = new SparseArray<>(0); /** Avoid creating display context frequently for non-default display. */ private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); private final Context mContext; private final DisplayController mDisplayController; + private final DisplayInsetsController mDisplayInsetsController; private final DisplayImeController mImeController; private final SyncTransactionQueue mSyncQueue; + private final ShellExecutor mMainExecutor; + private final CompatUIImpl mImpl = new CompatUIImpl(); - private SizeCompatUICallback mCallback; + private CompatUICallback mCallback; /** Only show once automatically in the process life. */ private boolean mHasShownHint; + /** Indicates if the keyguard is currently occluded, in which case compat UIs shouldn't + * be shown. */ + private boolean mKeyguardOccluded; - public SizeCompatUIController(Context context, + public CompatUIController(Context context, DisplayController displayController, + DisplayInsetsController displayInsetsController, DisplayImeController imeController, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, + ShellExecutor mainExecutor) { mContext = context; mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; mImeController = imeController; mSyncQueue = syncQueue; + mMainExecutor = mainExecutor; mDisplayController.addDisplayWindowListener(this); mImeController.addPositionProcessor(this); } + /** Returns implementation of {@link CompatUI}. */ + public CompatUI asCompatUI() { + return mImpl; + } + /** Sets the callback for UI interactions. */ - public void setSizeCompatUICallback(SizeCompatUICallback callback) { + public void setCompatUICallback(CompatUICallback callback) { mCallback = callback; } /** - * Called when the Task info changed. Creates and updates the size compat UI if there is an + * Called when the Task info changed. Creates and updates the compat UI if there is an * activity in size compat, or removes the UI if there is no size compat activity. + * * @param displayId display the task and activity are in. * @param taskId task the activity is in. - * @param taskConfig task config to place the size compat UI with. + * @param taskConfig task config to place the compat UI with. * @param taskListener listener to handle the Task Surface placement. */ - public void onSizeCompatInfoChanged(int displayId, int taskId, + public void onCompatInfoChanged(int displayId, int taskId, @Nullable Configuration taskConfig, @Nullable ShellTaskOrganizer.TaskListener taskListener) { if (taskConfig == null || taskListener == null) { - // Null token means the current foreground activity is not in size compatibility mode. + // Null token means the current foreground activity is not in compatibility mode. removeLayout(taskId); } else if (mActiveLayouts.contains(taskId)) { // UI already exists, update the UI layout. updateLayout(taskId, taskConfig, taskListener); } else { - // Create a new size compat UI. + // Create a new compat UI. createLayout(displayId, taskId, taskConfig, taskListener); } } @Override + public void onDisplayAdded(int displayId) { + addOnInsetsChangedListener(displayId); + } + + @Override public void onDisplayRemoved(int displayId) { mDisplayContextCache.remove(displayId); + removeOnInsetsChangedListener(displayId); - // Remove all size compat UIs on the removed display. + // Remove all compat UIs on the removed display. final List<Integer> toRemoveTaskIds = new ArrayList<>(); forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId())); for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) { @@ -124,8 +160,29 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang } } + private void addOnInsetsChangedListener(int displayId) { + PerDisplayOnInsetsChangedListener listener = new PerDisplayOnInsetsChangedListener( + displayId); + listener.register(); + mOnInsetsChangedListeners.put(displayId, listener); + } + + private void removeOnInsetsChangedListener(int displayId) { + PerDisplayOnInsetsChangedListener listener = mOnInsetsChangedListeners.get(displayId); + if (listener == null) { + return; + } + listener.unregister(); + mOnInsetsChangedListeners.remove(displayId); + } + + @Override public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + updateDisplayLayout(displayId); + } + + private void updateDisplayLayout(int displayId) { final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId); forAllLayoutsOnDisplay(displayId, layout -> layout.updateDisplayLayout(displayLayout)); } @@ -138,8 +195,20 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang mDisplaysWithIme.remove(displayId); } - // Hide the size compat UIs when input method is showing. - forAllLayoutsOnDisplay(displayId, layout -> layout.updateImeVisibility(isShowing)); + // Hide the compat UIs when input method is showing. + forAllLayoutsOnDisplay(displayId, + layout -> layout.updateVisibility(showOnDisplay(displayId))); + } + + @VisibleForTesting + void onKeyguardOccludedChanged(boolean occluded) { + mKeyguardOccluded = occluded; + // Hide the compat UIs when keyguard is occluded. + forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); + } + + private boolean showOnDisplay(int displayId) { + return !mKeyguardOccluded && !isImeShowingOnDisplay(displayId); } private boolean isImeShowingOnDisplay(int displayId) { @@ -154,35 +223,34 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang return; } - final SizeCompatUILayout layout = createLayout(context, displayId, taskId, taskConfig, - taskListener); - mActiveLayouts.put(taskId, layout); - layout.createSizeCompatButton(isImeShowingOnDisplay(displayId)); + final CompatUIWindowManager compatUIWindowManager = + createLayout(context, displayId, taskId, taskConfig, taskListener); + mActiveLayouts.put(taskId, compatUIWindowManager); + compatUIWindowManager.createLayout(showOnDisplay(displayId)); } @VisibleForTesting - SizeCompatUILayout createLayout(Context context, int displayId, int taskId, + CompatUIWindowManager createLayout(Context context, int displayId, int taskId, Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { - final SizeCompatUILayout layout = new SizeCompatUILayout(mSyncQueue, mCallback, context, - taskConfig, taskId, taskListener, mDisplayController.getDisplayLayout(displayId), - mHasShownHint); + final CompatUIWindowManager compatUIWindowManager = new CompatUIWindowManager(context, + taskConfig, mSyncQueue, mCallback, taskId, taskListener, + mDisplayController.getDisplayLayout(displayId), mHasShownHint); // Only show hint for the first time. mHasShownHint = true; - return layout; + return compatUIWindowManager; } private void updateLayout(int taskId, Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { - final SizeCompatUILayout layout = mActiveLayouts.get(taskId); + final CompatUIWindowManager layout = mActiveLayouts.get(taskId); if (layout == null) { return; } - layout.updateSizeCompatInfo(taskConfig, taskListener, - isImeShowingOnDisplay(layout.getDisplayId())); + layout.updateCompatInfo(taskConfig, taskListener, showOnDisplay(layout.getDisplayId())); } private void removeLayout(int taskId) { - final SizeCompatUILayout layout = mActiveLayouts.get(taskId); + final CompatUIWindowManager layout = mActiveLayouts.get(taskId); if (layout != null) { layout.release(); mActiveLayouts.remove(taskId); @@ -208,13 +276,68 @@ public class SizeCompatUIController implements DisplayController.OnDisplaysChang return context; } - private void forAllLayoutsOnDisplay(int displayId, Consumer<SizeCompatUILayout> callback) { + private void forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManager> callback) { + forAllLayouts(layout -> layout.getDisplayId() == displayId, callback); + } + + private void forAllLayouts(Consumer<CompatUIWindowManager> callback) { + forAllLayouts(layout -> true, callback); + } + + private void forAllLayouts(Predicate<CompatUIWindowManager> condition, + Consumer<CompatUIWindowManager> callback) { for (int i = 0; i < mActiveLayouts.size(); i++) { final int taskId = mActiveLayouts.keyAt(i); - final SizeCompatUILayout layout = mActiveLayouts.get(taskId); - if (layout != null && layout.getDisplayId() == displayId) { + final CompatUIWindowManager layout = mActiveLayouts.get(taskId); + if (layout != null && condition.test(layout)) { callback.accept(layout); } } } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + @ExternalThread + private class CompatUIImpl implements CompatUI { + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + mMainExecutor.execute(() -> { + CompatUIController.this.onKeyguardOccludedChanged(occluded); + }); + } + } + + /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ + private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener { + final int mDisplayId; + final InsetsState mInsetsState = new InsetsState(); + + PerDisplayOnInsetsChangedListener(int displayId) { + mDisplayId = displayId; + } + + void register() { + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); + } + + void unregister() { + mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + if (mInsetsState.equals(insetsState)) { + return; + } + mInsetsState.set(insetsState); + updateDisplayLayout(mDisplayId); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsChanged(insetsState); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java new file mode 100644 index 000000000000..ea4f20968438 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 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.compatui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.wm.shell.R; + +/** + * Container for compat UI controls. + */ +public class CompatUILayout extends LinearLayout { + + private CompatUIWindowManager mWindowManager; + + public CompatUILayout(Context context) { + this(context, null); + } + + public CompatUILayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CompatUILayout(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CompatUILayout(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + void inject(CompatUIWindowManager windowManager) { + mWindowManager = windowManager; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + // Need to relayout after changes like hiding / showing a hint since they affect size. + // Doing this directly in setSizeCompatHintVisibility can result in flaky animation. + mWindowManager.relayout(); + } + + void setSizeCompatHintVisibility(boolean show) { + final LinearLayout sizeCompatHint = findViewById(R.id.size_compat_hint); + int visibility = show ? View.VISIBLE : View.GONE; + if (sizeCompatHint.getVisibility() == visibility) { + return; + } + sizeCompatHint.setVisibility(visibility); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + final ImageButton restartButton = findViewById(R.id.size_compat_restart_button); + restartButton.setOnClickListener(view -> mWindowManager.onRestartButtonClicked()); + restartButton.setOnLongClickListener(view -> { + mWindowManager.onRestartButtonLongClicked(); + return true; + }); + + final LinearLayout sizeCompatHint = findViewById(R.id.size_compat_hint); + ((TextView) sizeCompatHint.findViewById(R.id.compat_mode_hint_text)) + .setText(R.string.restart_button_description); + sizeCompatHint.setOnClickListener(view -> setSizeCompatHintVisibility(/* show= */ false)); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java new file mode 100644 index 000000000000..997ad04e3b57 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2021 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.compatui; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.util.Log; +import android.view.IWindow; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Holds view hierarchy of a root surface and helps to inflate and manage layout for compat + * controls. + */ +class CompatUIWindowManager extends WindowlessWindowManager { + + private static final String TAG = "CompatUIWindowManager"; + + private final SyncTransactionQueue mSyncQueue; + private final CompatUIController.CompatUICallback mCallback; + private final int mDisplayId; + private final int mTaskId; + private final Rect mStableBounds; + + private Context mContext; + private Configuration mTaskConfig; + private ShellTaskOrganizer.TaskListener mTaskListener; + private DisplayLayout mDisplayLayout; + + @VisibleForTesting + boolean mShouldShowHint; + + @Nullable + @VisibleForTesting + CompatUILayout mCompatUILayout; + + @Nullable + private SurfaceControlViewHost mViewHost; + @Nullable + private SurfaceControl mLeash; + + CompatUIWindowManager(Context context, Configuration taskConfig, + SyncTransactionQueue syncQueue, CompatUIController.CompatUICallback callback, + int taskId, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, + boolean hasShownHint) { + super(taskConfig, null /* rootSurface */, null /* hostInputToken */); + mContext = context; + mSyncQueue = syncQueue; + mCallback = callback; + mTaskConfig = taskConfig; + mDisplayId = mContext.getDisplayId(); + mTaskId = taskId; + mTaskListener = taskListener; + mDisplayLayout = displayLayout; + mShouldShowHint = !hasShownHint; + mStableBounds = new Rect(); + mDisplayLayout.getStableBounds(mStableBounds); + } + + @Override + public void setConfiguration(Configuration configuration) { + super.setConfiguration(configuration); + mContext = mContext.createConfigurationContext(configuration); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("CompatUILeash") + .setHidden(false) + .setCallsite("CompatUIWindowManager#attachToParentSurface"); + attachToParentSurface(builder); + mLeash = builder.build(); + b.setParent(mLeash); + } + + /** Creates the layout for compat controls. */ + void createLayout(boolean show) { + if (!show || mCompatUILayout != null) { + // Wait until compat controls should be visible. + return; + } + + initCompatUi(); + updateSurfacePosition(); + + mCallback.onSizeCompatRestartButtonAppeared(mTaskId); + } + + /** Called when compat info changed. */ + void updateCompatInfo(Configuration taskConfig, + ShellTaskOrganizer.TaskListener taskListener, boolean show) { + final Configuration prevTaskConfig = mTaskConfig; + final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener; + mTaskConfig = taskConfig; + mTaskListener = taskListener; + + // Update configuration. + mContext = mContext.createConfigurationContext(taskConfig); + setConfiguration(taskConfig); + + if (mCompatUILayout == null || prevTaskListener != taskListener) { + // TaskListener changed, recreate the layout for new surface parent. + release(); + createLayout(show); + return; + } + + if (!taskConfig.windowConfiguration.getBounds() + .equals(prevTaskConfig.windowConfiguration.getBounds())) { + // Reposition the UI surfaces. + updateSurfacePosition(); + } + + if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) { + // Update layout for RTL. + mCompatUILayout.setLayoutDirection(taskConfig.getLayoutDirection()); + updateSurfacePosition(); + } + } + + /** Called when the visibility of the UI should change. */ + void updateVisibility(boolean show) { + if (mCompatUILayout == null) { + // Layout may not have been created because it was hidden previously. + createLayout(show); + return; + } + + // Hide compat UIs when IME is showing. + final int newVisibility = show ? View.VISIBLE : View.GONE; + if (mCompatUILayout.getVisibility() != newVisibility) { + mCompatUILayout.setVisibility(newVisibility); + } + } + + /** Called when display layout changed. */ + void updateDisplayLayout(DisplayLayout displayLayout) { + final Rect prevStableBounds = mStableBounds; + final Rect curStableBounds = new Rect(); + displayLayout.getStableBounds(curStableBounds); + mDisplayLayout = displayLayout; + if (!prevStableBounds.equals(curStableBounds)) { + // Stable bounds changed, update UI surface positions. + updateSurfacePosition(); + mStableBounds.set(curStableBounds); + } + } + + /** Called when it is ready to be placed compat UI surface. */ + void attachToParentSurface(SurfaceControl.Builder b) { + mTaskListener.attachChildSurfaceToTask(mTaskId, b); + } + + /** Called when the restart button is clicked. */ + void onRestartButtonClicked() { + mCallback.onSizeCompatRestartButtonClicked(mTaskId); + } + + /** Called when the restart button is long clicked. */ + void onRestartButtonLongClicked() { + if (mCompatUILayout == null) { + return; + } + mCompatUILayout.setSizeCompatHintVisibility(/* show= */ true); + } + + int getDisplayId() { + return mDisplayId; + } + + int getTaskId() { + return mTaskId; + } + + /** Releases the surface control and tears down the view hierarchy. */ + void release() { + mCompatUILayout = null; + + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + + if (mLeash != null) { + final SurfaceControl leash = mLeash; + mSyncQueue.runInSync(t -> t.remove(leash)); + mLeash = null; + } + } + + void relayout() { + mViewHost.relayout(getWindowLayoutParams()); + updateSurfacePosition(); + } + + @VisibleForTesting + void updateSurfacePosition() { + if (mCompatUILayout == null || mLeash == null) { + return; + } + + // Use stable bounds to prevent controls from overlapping with system bars. + final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds(); + final Rect stableBounds = new Rect(); + mDisplayLayout.getStableBounds(stableBounds); + stableBounds.intersect(taskBounds); + + // Position of the button in the container coordinate. + final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? stableBounds.left - taskBounds.left + : stableBounds.right - taskBounds.left - mCompatUILayout.getMeasuredWidth(); + final int positionY = stableBounds.bottom - taskBounds.top + - mCompatUILayout.getMeasuredHeight(); + + updateSurfacePosition(positionX, positionY); + } + + private int getLayoutDirection() { + return mContext.getResources().getConfiguration().getLayoutDirection(); + } + + private void updateSurfacePosition(int positionX, int positionY) { + mSyncQueue.runInSync(t -> { + if (mLeash == null || !mLeash.isValid()) { + Log.w(TAG, "The leash has been released."); + return; + } + t.setPosition(mLeash, positionX, positionY); + // The compat UI should be the topmost child of the Task in case there can be more + // than one children. + t.setLayer(mLeash, Integer.MAX_VALUE); + }); + } + + /** Inflates {@link CompatUILayout} on to the root surface. */ + private void initCompatUi() { + if (mViewHost != null) { + throw new IllegalStateException( + "A UI has already been created with this window manager."); + } + + // Construction extracted into the separate methods to allow injection for tests. + mViewHost = createSurfaceViewHost(); + mCompatUILayout = inflateCompatUILayout(); + mCompatUILayout.inject(this); + + mCompatUILayout.setSizeCompatHintVisibility(mShouldShowHint); + + mViewHost.setView(mCompatUILayout, getWindowLayoutParams()); + + // Only show by default for the first time. + mShouldShowHint = false; + } + + @VisibleForTesting + CompatUILayout inflateCompatUILayout() { + return (CompatUILayout) LayoutInflater.from(mContext) + .inflate(R.layout.compat_ui_layout, null); + } + + @VisibleForTesting + SurfaceControlViewHost createSurfaceViewHost() { + return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + } + + /** Gets the layout params. */ + private WindowManager.LayoutParams getWindowLayoutParams() { + // Measure how big the hint is since its size depends on the text size. + mCompatUILayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams( + // Cannot be wrap_content as this determines the actual window size + mCompatUILayout.getMeasuredWidth(), mCompatUILayout.getMeasuredHeight(), + TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT); + winParams.token = new Binder(); + winParams.setTitle(CompatUILayout.class.getSimpleName() + mTaskId); + winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + return winParams; + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java new file mode 100644 index 000000000000..806f795d1015 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 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.dagger; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** + * This is a qualifier that Shell uses to workaround an issue with providing nullable optionals + * which are by default unbound. + * + * For example, ideally we would have this scenario: + * BaseModule: + * @BindsOptionalOf + * abstract Optional<Interface> optionalInterface(); + * + * SpecializedModule: + * @Provides + * static Interface providesInterface() { + * return new InterfaceImpl(); + * } + * + * However, if the interface is supposed to be provided dynamically, then Dagger is not able to bind + * the optional interface to a null instance, and @BindsOptionalOf does not support @Nullable + * instances of the interface provided by the specialized module. + * + * For example, this does not work: + * BaseModule: + * @BindsOptionalOf + * abstract Optional<Interface> optionalInterface(); + * + * SpecializedModule: + * @Provides + * static Interface providesInterface() { + * if (systemSupportsInterfaceFeature) { + * return new InterfaceImpl(); + * } else { + * return null; + * } + * } + * + * To workaround this, we can instead upstream the check (assuming it can be upstreamed into the + * base module), and then always provide a non-null instance in the specialized module. + * + * For example: + * BaseModule: + * @BindsOptionalOf + * @DynamicOverride + * abstract Interface dynamicInterface(); + * + * @Provides + * static Optional<Interface> providesOptionalInterface( + * @DynamicOverride Optional<Interface> interface) { + * if (systemSupportsInterfaceFeature) { + * return interface; + * } + * return Optional.empty(); + * } + * + * SpecializedModule: + * @Provides + * @DynamicOverride + * static Interface providesInterface() { + * return new InterfaceImpl(); + * } + * + * This is also useful in cases where there needs to be a default implementation in the base module + * which is also overridable in the specialized module. This isn't generally recommended, but + * due to the nature of Shell modules being referenced from a number of various projects, this + * can be useful for *required* components that + * 1) clearly identifies which are intended for overriding in the base module, and + * 2) allows us to declare a default implementation in the base module, without having to force + * every SysUI impl to explicitly provide it (if a large number of them share the default impl) + * + * For example, this uses the same setup as above, but the interface provided (if bound) is used + * otherwise the default is created: + * @BindsOptionalOf + * @DynamicOverride + * abstract Interface dynamicInterface(); + * + * @Provides + * static Optional<Interface> providesOptionalInterface( + * @DynamicOverride Optional<Interface> overrideInterfaceImpl) { + * if (overrideInterfaceImpl.isPresent()) { + * return overrideInterfaceImpl.get(); + * } + * return new DefaultImpl(); + * } + * + * SpecializedModule: + * @Provides + * @DynamicOverride + * static Interface providesInterface() { + * return new SuperSpecialImpl(); + * } + */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface DynamicOverride {}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt new file mode 100644 index 000000000000..1cd69edf7cd2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/README.txt @@ -0,0 +1,13 @@ +The dagger modules in this directory can be included by the host SysUI using the Shell library for +explicity injection of Shell components. Apps using this library are not required to use these +dagger modules for setup, but it is recommended for them to include them as needed. + +The modules are currently inherited as such: + ++- WMShellBaseModule (common shell features across SysUI) + | + +- WMShellModule (handheld) + | + +- TvPipModule (tv pip) + | + +- TvWMShellModule (tv)
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java new file mode 100644 index 000000000000..711a0ac76702 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2021 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.dagger; + +import android.content.Context; +import android.os.Handler; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.PipTransitionState; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.tv.TvPipController; +import com.android.wm.shell.pip.tv.TvPipMenuController; +import com.android.wm.shell.pip.tv.TvPipNotificationController; +import com.android.wm.shell.pip.tv.TvPipTransition; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +import dagger.Module; +import dagger.Provides; + +/** + * Provides TV specific dependencies for Pip. + */ +@Module(includes = {WMShellBaseModule.class}) +public abstract class TvPipModule { + @WMSingleton + @Provides + static Optional<Pip> providePip( + Context context, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTaskOrganizer pipTaskOrganizer, + TvPipMenuController tvPipMenuController, + PipMediaController pipMediaController, + PipTransitionController pipTransitionController, + TvPipNotificationController tvPipNotificationController, + TaskStackListenerImpl taskStackListener, + WindowManagerShellWrapper windowManagerShellWrapper, + @ShellMainThread ShellExecutor mainExecutor) { + return Optional.of( + TvPipController.create( + context, + pipBoundsState, + pipBoundsAlgorithm, + pipTaskOrganizer, + pipTransitionController, + tvPipMenuController, + pipMediaController, + tvPipNotificationController, + taskStackListener, + windowManagerShellWrapper, + mainExecutor)); + } + + @WMSingleton + @Provides + static PipSnapAlgorithm providePipSnapAlgorithm() { + return new PipSnapAlgorithm(); + } + + @WMSingleton + @Provides + static PipBoundsAlgorithm providePipBoundsAlgorithm(Context context, + PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { + return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); + } + + @WMSingleton + @Provides + static PipBoundsState providePipBoundsState(Context context) { + return new PipBoundsState(context); + } + + // Handler needed for loadDrawableAsync() in PipControlsViewController + @WMSingleton + @Provides + static PipTransitionController provideTvPipTransition( + Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, + PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, TvPipMenuController pipMenuController) { + return new TvPipTransition(pipBoundsState, pipMenuController, + pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + } + + @WMSingleton + @Provides + static TvPipMenuController providesTvPipMenuController( + Context context, + PipBoundsState pipBoundsState, + SystemWindows systemWindows, + PipMediaController pipMediaController, + @ShellMainThread Handler mainHandler) { + return new TvPipMenuController(context, pipBoundsState, systemWindows, pipMediaController, + mainHandler); + } + + // Handler needed for registerReceiverForAllUsers() + @WMSingleton + @Provides + static TvPipNotificationController provideTvPipNotificationController(Context context, + PipMediaController pipMediaController, + @ShellMainThread Handler mainHandler) { + return new TvPipNotificationController(context, pipMediaController, mainHandler); + } + + @WMSingleton + @Provides + static PipAnimationController providePipAnimationController(PipSurfaceTransactionHelper + pipSurfaceTransactionHelper) { + return new PipAnimationController(pipSurfaceTransactionHelper); + } + + @WMSingleton + @Provides + static PipTransitionState providePipTransitionState() { + return new PipTransitionState(); + } + + @WMSingleton + @Provides + static PipTaskOrganizer providePipTaskOrganizer(Context context, + TvPipMenuController tvPipMenuController, + SyncTransactionQueue syncTransactionQueue, + PipBoundsState pipBoundsState, + PipTransitionState pipTransitionState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipAnimationController pipAnimationController, + PipTransitionController pipTransitionController, + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + Optional<LegacySplitScreenController> splitScreenOptional, + Optional<SplitScreenController> newSplitScreenOptional, + DisplayController displayController, + PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipTaskOrganizer(context, + syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, + tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, + pipTransitionController, splitScreenOptional, newSplitScreenOptional, + displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java new file mode 100644 index 000000000000..15bfeb297b41 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 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.dagger; + +import android.view.IWindowManager; + +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; +import com.android.wm.shell.startingsurface.tv.TvStartingWindowTypeAlgorithm; + +import dagger.Module; +import dagger.Provides; + +/** + * Provides dependencies from {@link com.android.wm.shell}, these dependencies are only + * accessible from components within the WM subcomponent (can be explicitly exposed to the + * SysUIComponent, see {@link WMComponent}). + * + * This module only defines Shell dependencies for the TV SystemUI implementation. Common + * dependencies should go into {@link WMShellBaseModule}. + */ +@Module(includes = {TvPipModule.class}) +public class TvWMShellModule { + + // + // Starting Windows (Splash Screen) + // + + @WMSingleton + @Provides + @DynamicOverride + static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm() { + return new TvStartingWindowTypeAlgorithm(); + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java new file mode 100644 index 000000000000..6d4b2fa60b55 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -0,0 +1,664 @@ +/* + * Copyright (C) 2021 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.dagger; + +import static com.android.wm.shell.onehanded.OneHandedController.SUPPORT_ONE_HANDED_MODE; + +import android.app.ActivityTaskManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.SystemProperties; +import android.view.IWindowManager; + +import com.android.internal.logging.UiEventLogger; +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootDisplayAreaOrganizer; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellCommandHandler; +import com.android.wm.shell.ShellCommandHandlerImpl; +import com.android.wm.shell.ShellInit; +import com.android.wm.shell.ShellInitImpl; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.TaskViewFactory; +import com.android.wm.shell.TaskViewFactoryController; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.apppairs.AppPairs; +import com.android.wm.shell.apppairs.AppPairsController; +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ShellAnimationThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.common.annotations.ShellSplashscreenThread; +import com.android.wm.shell.compatui.CompatUI; +import com.android.wm.shell.compatui.CompatUIController; +import com.android.wm.shell.displayareahelper.DisplayAreaHelper; +import com.android.wm.shell.displayareahelper.DisplayAreaHelperController; +import com.android.wm.shell.draganddrop.DragAndDrop; +import com.android.wm.shell.draganddrop.DragAndDropController; +import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.fullscreen.FullscreenUnfoldController; +import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout; +import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreen; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.onehanded.OneHanded; +import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PipAppOpsListener; +import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.recents.RecentTasks; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.startingsurface.StartingSurface; +import com.android.wm.shell.startingsurface.StartingWindowController; +import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm; +import com.android.wm.shell.startingsurface.phone.PhoneStartingWindowTypeAlgorithm; +import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelper; +import com.android.wm.shell.tasksurfacehelper.TaskSurfaceHelperController; +import com.android.wm.shell.transition.ShellTransitions; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; + +import java.util.Optional; + +import dagger.BindsOptionalOf; +import dagger.Module; +import dagger.Provides; + +/** + * Provides basic dependencies from {@link com.android.wm.shell}, these dependencies are only + * accessible from components within the WM subcomponent (can be explicitly exposed to the + * SysUIComponent, see {@link WMComponent}). + * + * This module only defines *common* dependencies across various SystemUI implementations, + * dependencies that are device/form factor SystemUI implementation specific should go into their + * respective modules (ie. {@link WMShellModule} for handheld, {@link TvWMShellModule} for tv, etc.) + */ +@Module(includes = WMShellConcurrencyModule.class) +public abstract class WMShellBaseModule { + + // + // Internal common - Components used internally by multiple shell features + // + + @WMSingleton + @Provides + static DisplayController provideDisplayController(Context context, + IWindowManager wmService, @ShellMainThread ShellExecutor mainExecutor) { + return new DisplayController(context, wmService, mainExecutor); + } + + @WMSingleton + @Provides + static DisplayInsetsController provideDisplayInsetsController( IWindowManager wmService, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor) { + return new DisplayInsetsController(wmService, displayController, mainExecutor); + } + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract DisplayImeController optionalDisplayImeController(); + + @WMSingleton + @Provides + static DisplayImeController provideDisplayImeController( + @DynamicOverride Optional<DisplayImeController> overrideDisplayImeController, + IWindowManager wmService, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + @ShellMainThread ShellExecutor mainExecutor, + TransactionPool transactionPool + ) { + if (overrideDisplayImeController.isPresent()) { + return overrideDisplayImeController.get(); + } + return new DisplayImeController(wmService, displayController, displayInsetsController, + mainExecutor, transactionPool); + } + + @WMSingleton + @Provides + static DisplayLayout provideDisplayLayout() { + return new DisplayLayout(); + } + + @WMSingleton + @Provides + static DragAndDropController provideDragAndDropController(Context context, + DisplayController displayController, UiEventLogger uiEventLogger, + IconProvider iconProvider, @ShellMainThread ShellExecutor mainExecutor) { + return new DragAndDropController(context, displayController, uiEventLogger, iconProvider, + mainExecutor); + } + + @WMSingleton + @Provides + static DragAndDrop provideDragAndDrop(DragAndDropController dragAndDropController) { + return dragAndDropController.asDragAndDrop(); + } + + @WMSingleton + @Provides + static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor, + Context context, + CompatUIController compatUI, + Optional<RecentTasksController> recentTasksOptional + ) { + return new ShellTaskOrganizer(mainExecutor, context, compatUI, recentTasksOptional); + } + + @WMSingleton + @Provides + static CompatUI provideCompatUI(CompatUIController compatUIController) { + return compatUIController.asCompatUI(); + } + + @WMSingleton + @Provides + static CompatUIController provideCompatUIController(Context context, + DisplayController displayController, DisplayInsetsController displayInsetsController, + DisplayImeController imeController, SyncTransactionQueue syncQueue, + @ShellMainThread ShellExecutor mainExecutor) { + return new CompatUIController(context, displayController, displayInsetsController, + imeController, syncQueue, mainExecutor); + } + + @WMSingleton + @Provides + static SyncTransactionQueue provideSyncTransactionQueue(TransactionPool pool, + @ShellMainThread ShellExecutor mainExecutor) { + return new SyncTransactionQueue(pool, mainExecutor); + } + + @WMSingleton + @Provides + static SystemWindows provideSystemWindows(DisplayController displayController, + IWindowManager wmService) { + return new SystemWindows(displayController, wmService); + } + + @WMSingleton + @Provides + static IconProvider provideIconProvider(Context context) { + return new IconProvider(context); + } + + // We currently dedupe multiple messages, so we use the shell main handler directly + @WMSingleton + @Provides + static TaskStackListenerImpl providerTaskStackListenerImpl( + @ShellMainThread Handler mainHandler) { + return new TaskStackListenerImpl(mainHandler); + } + + @WMSingleton + @Provides + static TransactionPool provideTransactionPool() { + return new TransactionPool(); + } + + @WMSingleton + @Provides + static WindowManagerShellWrapper provideWindowManagerShellWrapper( + @ShellMainThread ShellExecutor mainExecutor) { + return new WindowManagerShellWrapper(mainExecutor); + } + + // + // Bubbles (optional feature) + // + + @WMSingleton + @Provides + static Optional<Bubbles> provideBubbles(Optional<BubbleController> bubbleController) { + return bubbleController.map((controller) -> controller.asBubbles()); + } + + @BindsOptionalOf + abstract BubbleController optionalBubblesController(); + + // + // Fullscreen + // + + @WMSingleton + @Provides + static FullscreenTaskListener provideFullscreenTaskListener( + SyncTransactionQueue syncQueue, + Optional<FullscreenUnfoldController> optionalFullscreenUnfoldController, + Optional<RecentTasksController> recentTasksOptional + ) { + return new FullscreenTaskListener(syncQueue, optionalFullscreenUnfoldController, + recentTasksOptional); + } + + // + // Unfold transition + // + + @BindsOptionalOf + abstract ShellUnfoldProgressProvider optionalShellUnfoldProgressProvider(); + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract FullscreenUnfoldController optionalFullscreenUnfoldController(); + + @WMSingleton + @Provides + static Optional<FullscreenUnfoldController> provideFullscreenUnfoldController( + @DynamicOverride Optional<FullscreenUnfoldController> fullscreenUnfoldController, + Optional<ShellUnfoldProgressProvider> progressProvider) { + if (progressProvider.isPresent() + && progressProvider.get() != ShellUnfoldProgressProvider.NO_PROVIDER) { + return fullscreenUnfoldController; + } + return Optional.empty(); + } + + // + // Freeform (optional feature) + // + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract FreeformTaskListener optionalFreeformTaskListener(); + + @WMSingleton + @Provides + static Optional<FreeformTaskListener> provideFreeformTaskListener( + @DynamicOverride Optional<FreeformTaskListener> freeformTaskListener, + Context context) { + if (FreeformTaskListener.isFreeformEnabled(context)) { + return freeformTaskListener; + } + return Optional.empty(); + } + + // + // Hide display cutout + // + + @WMSingleton + @Provides + static Optional<HideDisplayCutout> provideHideDisplayCutout( + Optional<HideDisplayCutoutController> hideDisplayCutoutController) { + return hideDisplayCutoutController.map((controller) -> controller.asHideDisplayCutout()); + } + + @WMSingleton + @Provides + static Optional<HideDisplayCutoutController> provideHideDisplayCutoutController(Context context, + DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor) { + return Optional.ofNullable( + HideDisplayCutoutController.create(context, displayController, mainExecutor)); + } + + // + // One handed mode (optional feature) + // + + @WMSingleton + @Provides + static Optional<OneHanded> provideOneHanded(Optional<OneHandedController> oneHandedController) { + return oneHandedController.map((controller) -> controller.asOneHanded()); + } + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract OneHandedController optionalOneHandedController(); + + @WMSingleton + @Provides + static Optional<OneHandedController> providesOneHandedController( + @DynamicOverride Optional<OneHandedController> oneHandedController) { + if (SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) { + return oneHandedController; + } + return Optional.empty(); + } + + + // + // Task to Surface communication + // + + @WMSingleton + @Provides + static Optional<TaskSurfaceHelper> provideTaskSurfaceHelper( + Optional<TaskSurfaceHelperController> taskSurfaceController) { + return taskSurfaceController.map((controller) -> controller.asTaskSurfaceHelper()); + } + + @Provides + static Optional<TaskSurfaceHelperController> provideTaskSurfaceHelperController( + ShellTaskOrganizer taskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { + return Optional.ofNullable(new TaskSurfaceHelperController(taskOrganizer, mainExecutor)); + } + + // + // Pip (optional feature) + // + + @WMSingleton + @Provides + static FloatingContentCoordinator provideFloatingContentCoordinator() { + return new FloatingContentCoordinator(); + } + + @WMSingleton + @Provides + static PipAppOpsListener providePipAppOpsListener(Context context, + PipTouchHandler pipTouchHandler, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipAppOpsListener(context, pipTouchHandler.getMotionHelper(), mainExecutor); + } + + // Needs handler for registering broadcast receivers + @WMSingleton + @Provides + static PipMediaController providePipMediaController(Context context, + @ShellMainThread Handler mainHandler) { + return new PipMediaController(context, mainHandler); + } + + @WMSingleton + @Provides + static PipSurfaceTransactionHelper providePipSurfaceTransactionHelper() { + return new PipSurfaceTransactionHelper(); + } + + @WMSingleton + @Provides + static PipUiEventLogger providePipUiEventLogger(UiEventLogger uiEventLogger, + PackageManager packageManager) { + return new PipUiEventLogger(uiEventLogger, packageManager); + } + + @BindsOptionalOf + abstract PipTouchHandler optionalPipTouchHandler(); + + // + // Recent tasks + // + + @WMSingleton + @Provides + static Optional<RecentTasks> provideRecentTasks( + Optional<RecentTasksController> recentTasksController) { + return recentTasksController.map((controller) -> controller.asRecentTasks()); + } + + @WMSingleton + @Provides + static Optional<RecentTasksController> provideRecentTasksController( + Context context, + TaskStackListenerImpl taskStackListener, + @ShellMainThread ShellExecutor mainExecutor + ) { + return Optional.ofNullable( + RecentTasksController.create(context, taskStackListener, mainExecutor)); + } + + // + // Shell transitions + // + + @WMSingleton + @Provides + static ShellTransitions provideRemoteTransitions(Transitions transitions) { + return transitions.asRemoteTransitions(); + } + + @WMSingleton + @Provides + static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool, + DisplayController displayController, Context context, + @ShellMainThread ShellExecutor mainExecutor, + @ShellAnimationThread ShellExecutor animExecutor) { + return new Transitions(organizer, pool, displayController, context, mainExecutor, + animExecutor); + } + + // + // Display areas + // + + @WMSingleton + @Provides + static RootTaskDisplayAreaOrganizer provideRootTaskDisplayAreaOrganizer( + @ShellMainThread ShellExecutor mainExecutor, Context context) { + return new RootTaskDisplayAreaOrganizer(mainExecutor, context); + } + + @WMSingleton + @Provides + static RootDisplayAreaOrganizer provideRootDisplayAreaOrganizer( + @ShellMainThread ShellExecutor mainExecutor) { + return new RootDisplayAreaOrganizer(mainExecutor); + } + + @WMSingleton + @Provides + static Optional<DisplayAreaHelper> provideDisplayAreaHelper( + @ShellMainThread ShellExecutor mainExecutor, + RootDisplayAreaOrganizer rootDisplayAreaOrganizer) { + return Optional.of(new DisplayAreaHelperController(mainExecutor, + rootDisplayAreaOrganizer)); + } + + // + // Splitscreen (optional feature) + // + + @WMSingleton + @Provides + static Optional<SplitScreen> provideSplitScreen( + Optional<SplitScreenController> splitScreenController) { + return splitScreenController.map((controller) -> controller.asSplitScreen()); + } + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract SplitScreenController optionalSplitScreenController(); + + @WMSingleton + @Provides + static Optional<SplitScreenController> providesSplitScreenController( + @DynamicOverride Optional<SplitScreenController> splitscreenController, + Context context) { + if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) { + return splitscreenController; + } + return Optional.empty(); + } + + // Legacy split (optional feature) + + @WMSingleton + @Provides + static Optional<LegacySplitScreen> provideLegacySplitScreen( + Optional<LegacySplitScreenController> splitScreenController) { + return splitScreenController.map((controller) -> controller.asLegacySplitScreen()); + } + + @BindsOptionalOf + abstract LegacySplitScreenController optionalLegacySplitScreenController(); + + // App Pairs (optional feature) + + @WMSingleton + @Provides + static Optional<AppPairs> provideAppPairs(Optional<AppPairsController> appPairsController) { + return appPairsController.map((controller) -> controller.asAppPairs()); + } + + @BindsOptionalOf + abstract AppPairsController optionalAppPairs(); + + // + // Starting window + // + + @WMSingleton + @Provides + static Optional<StartingSurface> provideStartingSurface( + StartingWindowController startingWindowController) { + return Optional.of(startingWindowController.asStartingSurface()); + } + + @WMSingleton + @Provides + static StartingWindowController provideStartingWindowController(Context context, + @ShellSplashscreenThread ShellExecutor splashScreenExecutor, + StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, + TransactionPool pool) { + return new StartingWindowController(context, splashScreenExecutor, + startingWindowTypeAlgorithm, iconProvider, pool); + } + + // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride} + @BindsOptionalOf + @DynamicOverride + abstract StartingWindowTypeAlgorithm optionalStartingWindowTypeAlgorithm(); + + @WMSingleton + @Provides + static StartingWindowTypeAlgorithm provideStartingWindowTypeAlgorithm( + @DynamicOverride Optional<StartingWindowTypeAlgorithm> startingWindowTypeAlgorithm + ) { + if (startingWindowTypeAlgorithm.isPresent()) { + return startingWindowTypeAlgorithm.get(); + } + // Default to phone starting window type + return new PhoneStartingWindowTypeAlgorithm(); + } + + // + // Task view factory + // + + @WMSingleton + @Provides + static Optional<TaskViewFactory> provideTaskViewFactory( + TaskViewFactoryController taskViewFactoryController) { + return Optional.of(taskViewFactoryController.asTaskViewFactory()); + } + + @WMSingleton + @Provides + static TaskViewFactoryController provideTaskViewFactoryController( + ShellTaskOrganizer shellTaskOrganizer, + @ShellMainThread ShellExecutor mainExecutor, + SyncTransactionQueue syncQueue) { + return new TaskViewFactoryController(shellTaskOrganizer, mainExecutor, syncQueue); + } + + // + // Misc + // + + @WMSingleton + @Provides + static ShellInit provideShellInit(ShellInitImpl impl) { + return impl.asShellInit(); + } + + @WMSingleton + @Provides + static ShellInitImpl provideShellInitImpl(DisplayController displayController, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + DragAndDropController dragAndDropController, + ShellTaskOrganizer shellTaskOrganizer, + Optional<BubbleController> bubblesOptional, + Optional<SplitScreenController> splitScreenOptional, + Optional<AppPairsController> appPairsOptional, + Optional<PipTouchHandler> pipTouchHandlerOptional, + FullscreenTaskListener fullscreenTaskListener, + Optional<FullscreenUnfoldController> appUnfoldTransitionController, + Optional<FreeformTaskListener> freeformTaskListener, + Optional<RecentTasksController> recentTasksOptional, + Transitions transitions, + StartingWindowController startingWindow, + @ShellMainThread ShellExecutor mainExecutor) { + return new ShellInitImpl(displayController, + displayImeController, + displayInsetsController, + dragAndDropController, + shellTaskOrganizer, + bubblesOptional, + splitScreenOptional, + appPairsOptional, + pipTouchHandlerOptional, + fullscreenTaskListener, + appUnfoldTransitionController, + freeformTaskListener, + recentTasksOptional, + transitions, + startingWindow, + mainExecutor); + } + + /** + * Note, this is only optional because we currently pass this to the SysUI component scope and + * for non-primary users, we may inject a null-optional for that dependency. + */ + @WMSingleton + @Provides + static Optional<ShellCommandHandler> provideShellCommandHandler(ShellCommandHandlerImpl impl) { + return Optional.of(impl.asShellCommandHandler()); + } + + @WMSingleton + @Provides + static ShellCommandHandlerImpl provideShellCommandHandlerImpl( + ShellTaskOrganizer shellTaskOrganizer, + Optional<LegacySplitScreenController> legacySplitScreenOptional, + Optional<SplitScreenController> splitScreenOptional, + Optional<Pip> pipOptional, + Optional<OneHandedController> oneHandedOptional, + Optional<HideDisplayCutoutController> hideDisplayCutout, + Optional<AppPairsController> appPairsOptional, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor) { + return new ShellCommandHandlerImpl(shellTaskOrganizer, + legacySplitScreenOptional, splitScreenOptional, pipOptional, oneHandedOptional, + hideDisplayCutout, appPairsOptional, recentTasksOptional, mainExecutor); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java new file mode 100644 index 000000000000..5c205f97beb7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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.dagger; + +import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; + +import android.animation.AnimationHandler; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Trace; + +import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.wm.shell.common.HandlerExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ChoreographerSfVsync; +import com.android.wm.shell.common.annotations.ExternalMainThread; +import com.android.wm.shell.common.annotations.ShellAnimationThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.common.annotations.ShellSplashscreenThread; +import com.android.wm.shell.R; + +import dagger.Module; +import dagger.Provides; + +/** + * Provides basic concurrency-related dependencies from {@link com.android.wm.shell}, these + * dependencies are only accessible from components within the WM subcomponent. + */ +@Module +public abstract class WMShellConcurrencyModule { + + private static final int MSGQ_SLOW_DELIVERY_THRESHOLD_MS = 30; + private static final int MSGQ_SLOW_DISPATCH_THRESHOLD_MS = 30; + + /** + * Returns whether to enable a separate shell thread for the shell features. + */ + private static boolean enableShellMainThread(Context context) { + return context.getResources().getBoolean(R.bool.config_enableShellMainThread); + } + + // + // Shell Concurrency - Components used for managing threading in the Shell and SysUI + // + + + /** + * Provide a SysUI main-thread Handler. + * + * Prefer the Main Executor when possible. + */ + @Provides + @ExternalMainThread + public static Handler provideMainHandler() { + return new Handler(Looper.getMainLooper()); + } + + /** + * Provide a SysUI main-thread Executor. + */ + @WMSingleton + @Provides + @ExternalMainThread + public static ShellExecutor provideSysUIMainExecutor( + @ExternalMainThread Handler sysuiMainHandler) { + return new HandlerExecutor(sysuiMainHandler); + } + + /** + * Shell main-thread Handler, don't use this unless really necessary (ie. need to dedupe + * multiple types of messages, etc.) + */ + @WMSingleton + @Provides + @ShellMainThread + public static Handler provideShellMainHandler(Context context, + @ExternalMainThread Handler sysuiMainHandler) { + if (enableShellMainThread(context)) { + HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_DISPLAY); + mainThread.start(); + if (Build.IS_DEBUGGABLE) { + mainThread.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER); + mainThread.getLooper().setSlowLogThresholdMs(MSGQ_SLOW_DISPATCH_THRESHOLD_MS, + MSGQ_SLOW_DELIVERY_THRESHOLD_MS); + } + return Handler.createAsync(mainThread.getLooper()); + } + return sysuiMainHandler; + } + + /** + * Provide a Shell main-thread Executor. + */ + @WMSingleton + @Provides + @ShellMainThread + public static ShellExecutor provideShellMainExecutor(Context context, + @ShellMainThread Handler mainHandler, + @ExternalMainThread ShellExecutor sysuiMainExecutor) { + if (enableShellMainThread(context)) { + return new HandlerExecutor(mainHandler); + } + return sysuiMainExecutor; + } + + /** + * Provide a Shell animation-thread Executor. + */ + @WMSingleton + @Provides + @ShellAnimationThread + public static ShellExecutor provideShellAnimationExecutor() { + HandlerThread shellAnimationThread = new HandlerThread("wmshell.anim", + THREAD_PRIORITY_DISPLAY); + shellAnimationThread.start(); + if (Build.IS_DEBUGGABLE) { + shellAnimationThread.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER); + shellAnimationThread.getLooper().setSlowLogThresholdMs(MSGQ_SLOW_DISPATCH_THRESHOLD_MS, + MSGQ_SLOW_DELIVERY_THRESHOLD_MS); + } + return new HandlerExecutor(Handler.createAsync(shellAnimationThread.getLooper())); + } + + /** + * Provides a Shell splashscreen-thread Executor + */ + @WMSingleton + @Provides + @ShellSplashscreenThread + public static ShellExecutor provideSplashScreenExecutor() { + HandlerThread shellSplashscreenThread = new HandlerThread("wmshell.splashscreen", + THREAD_PRIORITY_TOP_APP_BOOST); + shellSplashscreenThread.start(); + return new HandlerExecutor(shellSplashscreenThread.getThreadHandler()); + } + + /** + * Provide a Shell main-thread AnimationHandler. The AnimationHandler can be set on + * {@link android.animation.ValueAnimator}s and will ensure that the animation will run on + * the Shell main-thread with the SF vsync. + */ + @WMSingleton + @Provides + @ChoreographerSfVsync + public static AnimationHandler provideShellMainExecutorSfVsyncAnimationHandler( + @ShellMainThread ShellExecutor mainExecutor) { + try { + AnimationHandler handler = new AnimationHandler(); + mainExecutor.executeBlocking(() -> { + // This is called on the animation thread since it calls + // Choreographer.getSfInstance() which returns a thread-local Choreographer instance + // that uses the SF vsync + handler.setProvider(new SfVsyncFrameCallbackProvider()); + }); + return handler; + } catch (InterruptedException e) { + throw new RuntimeException("Failed to initialize SfVsync animation handler in 1s", e); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java new file mode 100644 index 000000000000..f562fd9cf1af --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2021 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.dagger; + +import android.animation.AnimationHandler; +import android.content.Context; +import android.content.pm.LauncherApps; +import android.os.Handler; +import android.view.WindowManager; + +import com.android.internal.logging.UiEventLogger; +import com.android.internal.statusbar.IStatusBarService; +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.WindowManagerShellWrapper; +import com.android.wm.shell.apppairs.AppPairsController; +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ChoreographerSfVsync; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.freeform.FreeformTaskListener; +import com.android.wm.shell.fullscreen.FullscreenUnfoldController; +import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; +import com.android.wm.shell.onehanded.OneHandedController; +import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipAnimationController; +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipSnapAlgorithm; +import com.android.wm.shell.pip.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip.PipTaskOrganizer; +import com.android.wm.shell.pip.PipTransition; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.pip.PipTransitionState; +import com.android.wm.shell.pip.PipUiEventLogger; +import com.android.wm.shell.pip.phone.PhonePipMenuController; +import com.android.wm.shell.pip.phone.PipAppOpsListener; +import com.android.wm.shell.pip.phone.PipController; +import com.android.wm.shell.pip.phone.PipMotionHelper; +import com.android.wm.shell.pip.phone.PipTouchHandler; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.splitscreen.StageTaskUnfoldController; +import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.Optional; + +import javax.inject.Provider; + +import dagger.Lazy; +import dagger.Module; +import dagger.Provides; + +/** + * Provides dependencies from {@link com.android.wm.shell}, these dependencies are only + * accessible from components within the WM subcomponent (can be explicitly exposed to the + * SysUIComponent, see {@link WMComponent}). + * + * This module only defines Shell dependencies for handheld SystemUI implementation. Common + * dependencies should go into {@link WMShellBaseModule}. + */ +@Module(includes = WMShellBaseModule.class) +public class WMShellModule { + + // + // Bubbles + // + + // Note: Handler needed for LauncherApps.register + @WMSingleton + @Provides + static BubbleController provideBubbleController(Context context, + FloatingContentCoordinator floatingContentCoordinator, + IStatusBarService statusBarService, + WindowManager windowManager, + WindowManagerShellWrapper windowManagerShellWrapper, + LauncherApps launcherApps, + TaskStackListenerImpl taskStackListener, + UiEventLogger uiEventLogger, + ShellTaskOrganizer organizer, + DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler, + SyncTransactionQueue syncQueue) { + return BubbleController.create(context, null /* synchronizer */, + floatingContentCoordinator, statusBarService, windowManager, + windowManagerShellWrapper, launcherApps, taskStackListener, + uiEventLogger, organizer, displayController, mainExecutor, mainHandler, syncQueue); + } + + // + // Freeform + // + + @WMSingleton + @Provides + @DynamicOverride + static FreeformTaskListener provideFreeformTaskListener( + SyncTransactionQueue syncQueue) { + return new FreeformTaskListener(syncQueue); + } + + // + // One handed mode + // + + + // Needs the shell main handler for ContentObserver callbacks + @WMSingleton + @Provides + @DynamicOverride + static OneHandedController provideOneHandedController(Context context, + WindowManager windowManager, DisplayController displayController, + DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, + UiEventLogger uiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { + return OneHandedController.create(context, windowManager, + displayController, displayLayout, taskStackListener, uiEventLogger, mainExecutor, + mainHandler); + } + + // + // Splitscreen + // + + @WMSingleton + @Provides + @DynamicOverride + static SplitScreenController provideSplitScreenController( + ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, Context context, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + @ShellMainThread ShellExecutor mainExecutor, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, IconProvider iconProvider, + Optional<RecentTasksController> recentTasks, + Provider<Optional<StageTaskUnfoldController>> stageTaskUnfoldControllerProvider) { + return new SplitScreenController(shellTaskOrganizer, syncQueue, context, + rootTaskDisplayAreaOrganizer, mainExecutor, displayImeController, + displayInsetsController, transitions, transactionPool, iconProvider, + recentTasks, stageTaskUnfoldControllerProvider); + } + + @WMSingleton + @Provides + static LegacySplitScreenController provideLegacySplitScreen(Context context, + DisplayController displayController, SystemWindows systemWindows, + DisplayImeController displayImeController, TransactionPool transactionPool, + ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, + TaskStackListenerImpl taskStackListener, Transitions transitions, + @ShellMainThread ShellExecutor mainExecutor, + @ChoreographerSfVsync AnimationHandler sfVsyncAnimationHandler) { + return new LegacySplitScreenController(context, displayController, systemWindows, + displayImeController, transactionPool, shellTaskOrganizer, syncQueue, + taskStackListener, transitions, mainExecutor, sfVsyncAnimationHandler); + } + + @WMSingleton + @Provides + static AppPairsController provideAppPairs(ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, DisplayController displayController, + @ShellMainThread ShellExecutor mainExecutor, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController) { + return new AppPairsController(shellTaskOrganizer, syncQueue, displayController, + mainExecutor, displayImeController, displayInsetsController); + } + + // + // Pip + // + + @WMSingleton + @Provides + static Optional<Pip> providePip(Context context, DisplayController displayController, + PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer, + PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController, + WindowManagerShellWrapper windowManagerShellWrapper, + TaskStackListenerImpl taskStackListener, + Optional<OneHandedController> oneHandedController, + @ShellMainThread ShellExecutor mainExecutor) { + return Optional.ofNullable(PipController.create(context, displayController, + pipAppOpsListener, pipBoundsAlgorithm, pipBoundsState, pipMediaController, + phonePipMenuController, pipTaskOrganizer, pipTouchHandler, pipTransitionController, + windowManagerShellWrapper, taskStackListener, oneHandedController, mainExecutor)); + } + + @WMSingleton + @Provides + static PipBoundsState providePipBoundsState(Context context) { + return new PipBoundsState(context); + } + + @WMSingleton + @Provides + static PipSnapAlgorithm providePipSnapAlgorithm() { + return new PipSnapAlgorithm(); + } + + @WMSingleton + @Provides + static PipBoundsAlgorithm providesPipBoundsAlgorithm(Context context, + PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { + return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); + } + + // Handler is used by Icon.loadDrawableAsync + @WMSingleton + @Provides + static PhonePipMenuController providesPipPhoneMenuController(Context context, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + SystemWindows systemWindows, + Optional<SplitScreenController> splitScreenOptional, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { + return new PhonePipMenuController(context, pipBoundsState, pipMediaController, + systemWindows, splitScreenOptional, mainExecutor, mainHandler); + } + + @WMSingleton + @Provides + static PipTouchHandler providePipTouchHandler(Context context, + PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, + PipTaskOrganizer pipTaskOrganizer, + PipMotionHelper pipMotionHelper, + FloatingContentCoordinator floatingContentCoordinator, + PipUiEventLogger pipUiEventLogger, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipTouchHandler(context, menuPhoneController, pipBoundsAlgorithm, + pipBoundsState, pipTaskOrganizer, pipMotionHelper, + floatingContentCoordinator, pipUiEventLogger, mainExecutor); + } + + @WMSingleton + @Provides + static PipTransitionState providePipTransitionState() { + return new PipTransitionState(); + } + + @WMSingleton + @Provides + static PipTaskOrganizer providePipTaskOrganizer(Context context, + SyncTransactionQueue syncTransactionQueue, + PipTransitionState pipTransitionState, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PhonePipMenuController menuPhoneController, + PipAnimationController pipAnimationController, + PipSurfaceTransactionHelper pipSurfaceTransactionHelper, + PipTransitionController pipTransitionController, + Optional<LegacySplitScreenController> splitScreenOptional, + Optional<SplitScreenController> newSplitScreenOptional, + DisplayController displayController, + PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, + @ShellMainThread ShellExecutor mainExecutor) { + return new PipTaskOrganizer(context, + syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, + menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper, + pipTransitionController, splitScreenOptional, newSplitScreenOptional, + displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); + } + + @WMSingleton + @Provides + static PipAnimationController providePipAnimationController(PipSurfaceTransactionHelper + pipSurfaceTransactionHelper) { + return new PipAnimationController(pipSurfaceTransactionHelper); + } + + @WMSingleton + @Provides + static PipTransitionController providePipTransitionController(Context context, + Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, + PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipBoundsState pipBoundsState, PipTransitionState pipTransitionState, + PhonePipMenuController pipMenuController) { + return new PipTransition(context, pipBoundsState, pipTransitionState, pipMenuController, + pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + } + + @WMSingleton + @Provides + static PipMotionHelper providePipMotionHelper(Context context, + PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, + PhonePipMenuController menuController, PipSnapAlgorithm pipSnapAlgorithm, + PipTransitionController pipTransitionController, + FloatingContentCoordinator floatingContentCoordinator) { + return new PipMotionHelper(context, pipBoundsState, pipTaskOrganizer, + menuController, pipSnapAlgorithm, pipTransitionController, + floatingContentCoordinator); + } + + // + // Unfold transition + // + + @WMSingleton + @Provides + @DynamicOverride + static FullscreenUnfoldController provideFullscreenUnfoldController( + Context context, + Optional<ShellUnfoldProgressProvider> progressProvider, + Lazy<UnfoldBackgroundController> unfoldBackgroundController, + DisplayInsetsController displayInsetsController, + @ShellMainThread ShellExecutor mainExecutor + ) { + return new FullscreenUnfoldController(context, mainExecutor, + unfoldBackgroundController.get(), progressProvider.get(), + displayInsetsController); + } + + @Provides + static Optional<StageTaskUnfoldController> provideStageTaskUnfoldController( + Optional<ShellUnfoldProgressProvider> progressProvider, + Context context, + TransactionPool transactionPool, + Lazy<UnfoldBackgroundController> unfoldBackgroundController, + DisplayInsetsController displayInsetsController, + @ShellMainThread ShellExecutor mainExecutor + ) { + return progressProvider.map(shellUnfoldTransitionProgressProvider -> + new StageTaskUnfoldController( + context, + transactionPool, + shellUnfoldTransitionProgressProvider, + displayInsetsController, + unfoldBackgroundController.get(), + mainExecutor + )); + } + + @WMSingleton + @Provides + static UnfoldBackgroundController provideUnfoldBackgroundController( + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Context context + ) { + return new UnfoldBackgroundController( + context, + rootTaskDisplayAreaOrganizer + ); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMSingleton.java index 0037059e2c51..7f45c38d19a3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/Extensions.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMSingleton.java @@ -14,16 +14,20 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.pip +package com.android.wm.shell.dagger; -import android.content.ComponentName -import com.android.server.wm.traces.common.windowmanager.WindowManagerState -import com.android.server.wm.traces.parser.toWindowName +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Scope; /** - * Checks that an activity [activity] is in PIP mode + * Scope annotation for singleton items within the WMComponent. */ -fun WindowManagerState.isInPipMode(activity: ComponentName): Boolean { - val windowName = activity.toWindowName() - return isInPipMode(windowName) +@Documented +@Retention(RUNTIME) +@Scope +public @interface WMSingleton { } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java new file mode 100644 index 000000000000..defbd5af01d9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelper.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 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.displayareahelper; + +import android.view.SurfaceControl; + +import java.util.function.Consumer; + +/** + * Interface that allows to perform various display area related actions + */ +public interface DisplayAreaHelper { + + /** + * Updates SurfaceControl builder to reparent it to the root display area + * @param displayId id of the display to which root display area it should be reparented to + * @param builder surface control builder that should be updated + * @param onUpdated callback that is invoked after updating the builder, called on + * the shell main thread + */ + default void attachToRootDisplayArea(int displayId, SurfaceControl.Builder builder, + Consumer<SurfaceControl.Builder> onUpdated) { + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java new file mode 100644 index 000000000000..ef9ad6d10e6b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/displayareahelper/DisplayAreaHelperController.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 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.displayareahelper; + +import android.view.SurfaceControl; + +import com.android.wm.shell.RootDisplayAreaOrganizer; + +import java.util.concurrent.Executor; +import java.util.function.Consumer; + +public class DisplayAreaHelperController implements DisplayAreaHelper { + + private final Executor mExecutor; + private final RootDisplayAreaOrganizer mRootDisplayAreaOrganizer; + + public DisplayAreaHelperController(Executor executor, + RootDisplayAreaOrganizer rootDisplayAreaOrganizer) { + mExecutor = executor; + mRootDisplayAreaOrganizer = rootDisplayAreaOrganizer; + } + + @Override + public void attachToRootDisplayArea(int displayId, SurfaceControl.Builder builder, + Consumer<SurfaceControl.Builder> onUpdated) { + mExecutor.execute(() -> { + mRootDisplayAreaOrganizer.attachToDisplayArea(displayId, builder); + onUpdated.accept(builder); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java new file mode 100644 index 000000000000..edeff6e37182 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDrop.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 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.draganddrop; + +import android.content.res.Configuration; + +import com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface for telling DragAndDrop stuff. + */ +@ExternalThread +public interface DragAndDrop { + + /** Called when the theme changes. */ + void onThemeChanged(); + + /** Called when the configuration changes. */ + void onConfigChanged(Configuration newConfig); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index 58bf22ad29b2..101295d246bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -19,6 +19,7 @@ package com.android.wm.shell.draganddrop; import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.DragEvent.ACTION_DRAG_ENDED; import static android.view.DragEvent.ACTION_DRAG_ENTERED; import static android.view.DragEvent.ACTION_DRAG_EXITED; @@ -48,11 +49,14 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.UiEventLogger; import com.android.internal.protolog.common.ProtoLog; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.Optional; @@ -67,14 +71,27 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange private final Context mContext; private final DisplayController mDisplayController; + private final DragAndDropEventLogger mLogger; + private final IconProvider mIconProvider; private SplitScreenController mSplitScreen; + private ShellExecutor mMainExecutor; + private DragAndDropImpl mImpl; private final SparseArray<PerDisplay> mDisplayDropTargets = new SparseArray<>(); private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); - public DragAndDropController(Context context, DisplayController displayController) { + public DragAndDropController(Context context, DisplayController displayController, + UiEventLogger uiEventLogger, IconProvider iconProvider, ShellExecutor mainExecutor) { mContext = context; mDisplayController = displayController; + mLogger = new DragAndDropEventLogger(uiEventLogger); + mIconProvider = iconProvider; + mMainExecutor = mainExecutor; + mImpl = new DragAndDropImpl(); + } + + public DragAndDrop asDragAndDrop() { + return mImpl; } public void initialize(Optional<SplitScreenController> splitscreen) { @@ -85,6 +102,11 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange @Override public void onDisplayAdded(int displayId) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Display added: %d", displayId); + if (displayId != DEFAULT_DISPLAY) { + // Ignore non-default displays for now + return; + } + final Context context = mDisplayController.getDisplayContext(displayId) .createWindowContext(TYPE_APPLICATION_OVERLAY, null); final WindowManager wm = context.getSystemService(WindowManager.class); @@ -106,7 +128,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange R.layout.global_drop_target, null); rootView.setOnDragListener(this); rootView.setVisibility(View.INVISIBLE); - DragLayout dragLayout = new DragLayout(context, mSplitScreen); + DragLayout dragLayout = new DragLayout(context, mSplitScreen, mIconProvider); rootView.addView(dragLayout, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); try { @@ -175,9 +197,10 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange Slog.w(TAG, "Unexpected drag start during an active drag"); return false; } + InstanceId loggerSessionId = mLogger.logStart(event); pd.activeDragCount++; pd.dragLayout.prepare(mDisplayController.getDisplayLayout(displayId), - event.getClipData()); + event.getClipData(), loggerSessionId); setDropTargetWindowVisibility(pd, View.VISIBLE); break; case ACTION_DRAG_ENTERED: @@ -198,7 +221,9 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange case ACTION_DRAG_ENDED: // TODO(b/169894807): Ensure sure it's not possible to get ENDED without DROP // or EXITED - if (!pd.dragLayout.hasDropped()) { + if (pd.dragLayout.hasDropped()) { + mLogger.logDrop(); + } else { pd.activeDragCount--; pd.dragLayout.hide(event, () -> { if (pd.activeDragCount == 0) { @@ -208,6 +233,7 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange } }); } + mLogger.logEnd(); break; } return true; @@ -252,6 +278,18 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange return mimeTypes; } + private void onThemeChange() { + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + mDisplayDropTargets.get(i).dragLayout.onThemeChange(); + } + } + + private void onConfigChanged(Configuration newConfig) { + for (int i = 0; i < mDisplayDropTargets.size(); i++) { + mDisplayDropTargets.get(i).dragLayout.onConfigChanged(newConfig); + } + } + private static class PerDisplay { final int displayId; final Context context; @@ -272,4 +310,21 @@ public class DragAndDropController implements DisplayController.OnDisplaysChange dragLayout = dl; } } + + private class DragAndDropImpl implements DragAndDrop { + + @Override + public void onThemeChanged() { + mMainExecutor.execute(() -> { + DragAndDropController.this.onThemeChange(); + }); + } + + @Override + public void onConfigChanged(Configuration newConfig) { + mMainExecutor.execute(() -> { + DragAndDropController.this.onConfigChanged(newConfig); + }); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java new file mode 100644 index 000000000000..2a7dd5aeb341 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropEventLogger.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 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.draganddrop; + +import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; +import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.pm.ActivityInfo; +import android.view.DragEvent; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +/** + * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent + */ +public class DragAndDropEventLogger { + + private final UiEventLogger mUiEventLogger; + // Used to generate instance ids for this drag if one is not provided + private final InstanceIdSequence mIdSequence; + + // Tracks the current drag session + private ActivityInfo mActivityInfo; + private InstanceId mInstanceId; + + public DragAndDropEventLogger(UiEventLogger uiEventLogger) { + mUiEventLogger = uiEventLogger; + mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); + } + + /** + * Logs the start of a drag. + */ + public InstanceId logStart(DragEvent event) { + final ClipDescription description = event.getClipDescription(); + final ClipData data = event.getClipData(); + final ClipData.Item item = data.getItemAt(0); + mInstanceId = item.getIntent().getParcelableExtra( + ClipDescription.EXTRA_LOGGING_INSTANCE_ID); + if (mInstanceId == null) { + mInstanceId = mIdSequence.newInstanceId(); + } + mActivityInfo = item.getActivityInfo(); + log(getStartEnum(description), mActivityInfo); + return mInstanceId; + } + + /** + * Logs a successful drop. + */ + public void logDrop() { + log(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_DROPPED, mActivityInfo); + } + + /** + * Logs the end of a drag. + */ + public void logEnd() { + log(DragAndDropUiEventEnum.GLOBAL_APP_DRAG_END, mActivityInfo); + } + + private void log(UiEventLogger.UiEventEnum event, @Nullable ActivityInfo activityInfo) { + mUiEventLogger.logWithInstanceId(event, + activityInfo == null ? 0 : activityInfo.applicationInfo.uid, + activityInfo == null ? null : activityInfo.applicationInfo.packageName, + mInstanceId); + } + + /** + * Returns the start logging enum for the given drag description. + */ + private DragAndDropUiEventEnum getStartEnum(ClipDescription description) { + if (description.hasMimeType(MIMETYPE_APPLICATION_ACTIVITY)) { + return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_ACTIVITY; + } else if (description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT)) { + return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_SHORTCUT; + } else if (description.hasMimeType(MIMETYPE_APPLICATION_TASK)) { + return DragAndDropUiEventEnum.GLOBAL_APP_DRAG_START_TASK; + } + throw new IllegalArgumentException("Not an app drag"); + } + + /** + * Enums for logging Drag & Drop UiEvents + */ + public enum DragAndDropUiEventEnum implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Starting a global drag and drop of an activity") + GLOBAL_APP_DRAG_START_ACTIVITY(884), + + @UiEvent(doc = "Starting a global drag and drop of a shortcut") + GLOBAL_APP_DRAG_START_SHORTCUT(885), + + @UiEvent(doc = "Starting a global drag and drop of a task") + GLOBAL_APP_DRAG_START_TASK(888), + + @UiEvent(doc = "A global app drag was successfully dropped") + GLOBAL_APP_DRAG_DROPPED(887), + + @UiEvent(doc = "Ending a global app drag and drop") + GLOBAL_APP_DRAG_END(886); + + private final int mId; + + DragAndDropUiEventEnum(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index 9bcc3acf7a57..8e6c05d83906 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -29,16 +29,14 @@ import static android.content.Intent.EXTRA_SHORTCUT_ID; import static android.content.Intent.EXTRA_TASK_ID; import static android.content.Intent.EXTRA_USER; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import android.app.ActivityManager; import android.app.ActivityTaskManager; @@ -63,9 +61,9 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.internal.logging.InstanceId; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.split.SplitLayout.SplitPosition; -import com.android.wm.shell.splitscreen.SplitScreen.StageType; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreenController; import java.lang.annotation.Retention; @@ -86,6 +84,7 @@ public class DragAndDropPolicy { private final SplitScreenController mSplitScreen; private final ArrayList<DragAndDropPolicy.Target> mTargets = new ArrayList<>(); + private InstanceId mLoggerSessionId; private DragSession mSession; public DragAndDropPolicy(Context context, SplitScreenController splitScreen) { @@ -104,7 +103,8 @@ public class DragAndDropPolicy { /** * Starts a new drag session with the given initial drag data. */ - void start(DisplayLayout displayLayout, ClipData data) { + void start(DisplayLayout displayLayout, ClipData data, InstanceId loggerSessionId) { + mLoggerSessionId = loggerSessionId; mSession = new DragSession(mContext, mActivityTaskManager, displayLayout, data); // TODO(b/169894807): Also update the session data with task stack changes mSession.update(); @@ -151,10 +151,14 @@ public class DragAndDropPolicy { final Rect rightHitRegion = new Rect(); final Rect rightDrawRegion = bottomOrRightBounds; - displayRegion.splitVertically(leftHitRegion, fullscreenHitRegion, rightHitRegion); + // If we have existing split regions use those bounds, otherwise split it 50/50 + if (inSplitScreen) { + leftHitRegion.set(topOrLeftBounds); + rightHitRegion.set(bottomOrRightBounds); + } else { + displayRegion.splitVertically(leftHitRegion, rightHitRegion); + } - mTargets.add( - new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, leftDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, rightDrawRegion)); @@ -164,11 +168,14 @@ public class DragAndDropPolicy { final Rect bottomHitRegion = new Rect(); final Rect bottomDrawRegion = bottomOrRightBounds; - displayRegion.splitHorizontally( - topHitRegion, fullscreenHitRegion, bottomHitRegion); + // If we have existing split regions use those bounds, otherwise split it 50/50 + if (inSplitScreen) { + topHitRegion.set(topOrLeftBounds); + bottomHitRegion.set(bottomOrRightBounds); + } else { + displayRegion.splitHorizontally(topHitRegion, bottomHitRegion); + } - mTargets.add( - new Target(TYPE_FULLSCREEN, fullscreenHitRegion, fullscreenDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topDrawRegion)); mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomDrawRegion)); } @@ -199,27 +206,23 @@ public class DragAndDropPolicy { return; } - final boolean inSplitScreen = mSplitScreen != null && mSplitScreen.isSplitScreenVisible(); final boolean leftOrTop = target.type == TYPE_SPLIT_TOP || target.type == TYPE_SPLIT_LEFT; - @StageType int stage = STAGE_TYPE_UNDEFINED; @SplitPosition int position = SPLIT_POSITION_UNDEFINED; if (target.type != TYPE_FULLSCREEN && mSplitScreen != null) { // Update launch options for the split side we are targeting. position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; - if (!inSplitScreen) { - // Launch in the side stage if we are not in split-screen already. - stage = STAGE_TYPE_SIDE; - } + // Add some data for logging splitscreen once it is invoked + mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId); } final ClipDescription description = data.getDescription(); final Intent dragData = mSession.dragData; - startClipDescription(description, dragData, stage, position); + startClipDescription(description, dragData, position); } private void startClipDescription(ClipDescription description, Intent intent, - @StageType int stage, @SplitPosition int position) { + @SplitPosition int position) { final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); final Bundle opts = intent.hasExtra(EXTRA_ACTIVITY_OPTIONS) @@ -227,15 +230,15 @@ public class DragAndDropPolicy { if (isTask) { final int taskId = intent.getIntExtra(EXTRA_TASK_ID, INVALID_TASK_ID); - mStarter.startTask(taskId, stage, position, opts); + mStarter.startTask(taskId, position, opts); } else if (isShortcut) { final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); final String id = intent.getStringExtra(EXTRA_SHORTCUT_ID); final UserHandle user = intent.getParcelableExtra(EXTRA_USER); - mStarter.startShortcut(packageName, id, stage, position, opts, user); + mStarter.startShortcut(packageName, id, position, opts, user); } else { mStarter.startIntent(intent.getParcelableExtra(EXTRA_PENDING_INTENT), - null, stage, position, opts); + null, position, opts); } } @@ -269,7 +272,6 @@ public class DragAndDropPolicy { * Updates the session data based on the current state of the system. */ void update() { - List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1, false /* filterOnlyVisibleRecents */); if (!tasks.isEmpty()) { @@ -291,15 +293,18 @@ public class DragAndDropPolicy { * Interface for actually committing the task launches. */ public interface Starter { - void startTask(int taskId, @StageType int stage, @SplitPosition int position, - @Nullable Bundle options); - void startShortcut(String packageName, String shortcutId, @StageType int stage, - @SplitPosition int position, @Nullable Bundle options, UserHandle user); - void startIntent(PendingIntent intent, Intent fillInIntent, - @StageType int stage, @SplitPosition int position, + void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options); + void startShortcut(String packageName, String shortcutId, @SplitPosition int position, + @Nullable Bundle options, UserHandle user); + void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options); void enterSplitScreen(int taskId, boolean leftOrTop); - void exitSplitScreen(); + + /** + * Exits splitscreen, with an associated exit trigger from the SplitscreenUIChanged proto + * for logging. + */ + void exitSplitScreen(int toTopTaskId, int exitTrigger); } /** @@ -314,8 +319,7 @@ public class DragAndDropPolicy { } @Override - public void startTask(int taskId, int stage, int position, - @Nullable Bundle options) { + public void startTask(int taskId, int position, @Nullable Bundle options) { try { ActivityTaskManager.getService().startActivityFromRecents(taskId, options); } catch (RemoteException e) { @@ -324,7 +328,7 @@ public class DragAndDropPolicy { } @Override - public void startShortcut(String packageName, String shortcutId, int stage, int position, + public void startShortcut(String packageName, String shortcutId, int position, @Nullable Bundle options, UserHandle user) { try { LauncherApps launcherApps = @@ -337,8 +341,8 @@ public class DragAndDropPolicy { } @Override - public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, int stage, - int position, @Nullable Bundle options) { + public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent, int position, + @Nullable Bundle options) { try { intent.send(mContext, 0, fillInIntent, null, null, null, options); } catch (PendingIntent.CanceledException e) { @@ -352,7 +356,7 @@ public class DragAndDropPolicy { } @Override - public void exitSplitScreen() { + public void exitSplitScreen(int toTopTaskId, int exitTrigger) { throw new UnsupportedOperationException("exitSplitScreen not implemented by starter"); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index b3423362347f..fd3be2b11c15 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -16,77 +16,143 @@ package com.android.wm.shell.draganddrop; -import static com.android.wm.shell.animation.Interpolators.FAST_OUT_LINEAR_IN; -import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; -import static com.android.wm.shell.animation.Interpolators.LINEAR; -import static com.android.wm.shell.animation.Interpolators.LINEAR_OUT_SLOW_IN; +import static android.app.StatusBarManager.DISABLE_NONE; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.StatusBarManager; import android.content.ClipData; import android.content.Context; -import android.graphics.Canvas; +import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.RemoteException; import android.view.DragEvent; import android.view.SurfaceControl; -import android.view.View; import android.view.WindowInsets; import android.view.WindowInsets.Type; +import android.widget.LinearLayout; -import androidx.annotation.NonNull; - +import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; import java.util.ArrayList; +import java.util.List; /** * Coordinates the visible drop targets for the current drag. */ -public class DragLayout extends View { +public class DragLayout extends LinearLayout { + + // While dragging the status bar is hidden. + private static final int HIDE_STATUS_BAR_FLAGS = StatusBarManager.DISABLE_NOTIFICATION_ICONS + | StatusBarManager.DISABLE_NOTIFICATION_ALERTS + | StatusBarManager.DISABLE_CLOCK + | StatusBarManager.DISABLE_SYSTEM_INFO; private final DragAndDropPolicy mPolicy; + private final SplitScreenController mSplitScreenController; + private final IconProvider mIconProvider; + private final StatusBarManager mStatusBarManager; private DragAndDropPolicy.Target mCurrentTarget = null; - private DropOutlineDrawable mDropOutline; + private DropZoneView mDropZoneView1; + private DropZoneView mDropZoneView2; + private int mDisplayMargin; + private int mDividerSize; private Insets mInsets = Insets.NONE; private boolean mIsShowing; private boolean mHasDropped; - public DragLayout(Context context, SplitScreenController splitscreen) { + @SuppressLint("WrongConstant") + public DragLayout(Context context, SplitScreenController splitScreenController, + IconProvider iconProvider) { super(context); - mPolicy = new DragAndDropPolicy(context, splitscreen); + mSplitScreenController = splitScreenController; + mIconProvider = iconProvider; + mPolicy = new DragAndDropPolicy(context, splitScreenController); + mStatusBarManager = context.getSystemService(StatusBarManager.class); + mDisplayMargin = context.getResources().getDimensionPixelSize( R.dimen.drop_layout_display_margin); - mDropOutline = new DropOutlineDrawable(context); - setBackground(mDropOutline); - setWillNotDraw(false); + mDividerSize = context.getResources().getDimensionPixelSize( + R.dimen.split_divider_bar_width); + + mDropZoneView1 = new DropZoneView(context); + mDropZoneView2 = new DropZoneView(context); + addView(mDropZoneView1, new LinearLayout.LayoutParams(MATCH_PARENT, + MATCH_PARENT)); + addView(mDropZoneView2, new LinearLayout.LayoutParams(MATCH_PARENT, + MATCH_PARENT)); + ((LayoutParams) mDropZoneView1.getLayoutParams()).weight = 1; + ((LayoutParams) mDropZoneView2.getLayoutParams()).weight = 1; + updateContainerMargins(); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { mInsets = insets.getInsets(Type.systemBars() | Type.displayCutout()); recomputeDropTargets(); + + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + mDropZoneView1.setBottomInset(mInsets.bottom); + mDropZoneView2.setBottomInset(mInsets.bottom); + } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + mDropZoneView1.setBottomInset(0); + mDropZoneView2.setBottomInset(mInsets.bottom); + } return super.onApplyWindowInsets(insets); } - @Override - protected boolean verifyDrawable(@NonNull Drawable who) { - return who == mDropOutline || super.verifyDrawable(who); + public void onThemeChange() { + mDropZoneView1.onThemeChange(); + mDropZoneView2.onThemeChange(); } - @Override - protected void onDraw(Canvas canvas) { - if (mCurrentTarget != null) { - mDropOutline.draw(canvas); + public void onConfigChanged(Configuration newConfig) { + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE + && getOrientation() != HORIZONTAL) { + setOrientation(LinearLayout.HORIZONTAL); + updateContainerMargins(); + } else if (orientation == Configuration.ORIENTATION_PORTRAIT + && getOrientation() != VERTICAL) { + setOrientation(LinearLayout.VERTICAL); + updateContainerMargins(); + } + } + + private void updateContainerMargins() { + final int orientation = getResources().getConfiguration().orientation; + final float halfMargin = mDisplayMargin / 2f; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + mDropZoneView1.setContainerMargin( + mDisplayMargin, mDisplayMargin, halfMargin, mDisplayMargin); + mDropZoneView2.setContainerMargin( + halfMargin, mDisplayMargin, mDisplayMargin, mDisplayMargin); + } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + mDropZoneView1.setContainerMargin( + mDisplayMargin, mDisplayMargin, mDisplayMargin, halfMargin); + mDropZoneView2.setContainerMargin( + mDisplayMargin, halfMargin, mDisplayMargin, mDisplayMargin); } } @@ -98,10 +164,86 @@ public class DragLayout extends View { return mHasDropped; } - public void prepare(DisplayLayout displayLayout, ClipData initialData) { - mPolicy.start(displayLayout, initialData); + public void prepare(DisplayLayout displayLayout, ClipData initialData, + InstanceId loggerSessionId) { + mPolicy.start(displayLayout, initialData, loggerSessionId); mHasDropped = false; mCurrentTarget = null; + + boolean alreadyInSplit = mSplitScreenController != null + && mSplitScreenController.isSplitScreenVisible(); + if (!alreadyInSplit) { + List<ActivityManager.RunningTaskInfo> tasks = null; + // Figure out the splashscreen info for the existing task. + try { + tasks = ActivityTaskManager.getService().getTasks(1, + false /* filterOnlyVisibleRecents */, + false /* keepIntentExtra */); + } catch (RemoteException e) { + // don't show an icon / will just use the defaults + } + if (tasks != null && !tasks.isEmpty()) { + ActivityManager.RunningTaskInfo taskInfo1 = tasks.get(0); + Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo); + int bgColor1 = getResizingBackgroundColor(taskInfo1); + mDropZoneView1.setAppInfo(bgColor1, icon1); + mDropZoneView2.setAppInfo(bgColor1, icon1); + updateDropZoneSizes(null, null); // passing null splits the views evenly + } + } else { + // We're already in split so get taskInfo from the controller to populate icon / color. + ActivityManager.RunningTaskInfo topOrLeftTask = + mSplitScreenController.getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); + ActivityManager.RunningTaskInfo bottomOrRightTask = + mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT); + if (topOrLeftTask != null && bottomOrRightTask != null) { + Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo); + int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask); + Drawable bottomOrRightIcon = mIconProvider.getIcon( + bottomOrRightTask.topActivityInfo); + int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask); + mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon); + mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon); + } + + // Update the dropzones to match existing split sizes + Rect topOrLeftBounds = new Rect(); + Rect bottomOrRightBounds = new Rect(); + mSplitScreenController.getStageBounds(topOrLeftBounds, bottomOrRightBounds); + updateDropZoneSizes(topOrLeftBounds, bottomOrRightBounds); + } + } + + /** + * Sets the size of the two drop zones based on the provided bounds. The divider sits between + * the views and its size is included in the calculations. + * + * @param bounds1 bounds to apply to the first dropzone view, null if split in half. + * @param bounds2 bounds to apply to the second dropzone view, null if split in half. + */ + private void updateDropZoneSizes(Rect bounds1, Rect bounds2) { + final int orientation = getResources().getConfiguration().orientation; + final boolean isPortrait = orientation == Configuration.ORIENTATION_PORTRAIT; + final int halfDivider = mDividerSize / 2; + final LinearLayout.LayoutParams dropZoneView1 = + (LayoutParams) mDropZoneView1.getLayoutParams(); + final LinearLayout.LayoutParams dropZoneView2 = + (LayoutParams) mDropZoneView2.getLayoutParams(); + if (isPortrait) { + dropZoneView1.width = MATCH_PARENT; + dropZoneView2.width = MATCH_PARENT; + dropZoneView1.height = bounds1 != null ? bounds1.height() + halfDivider : MATCH_PARENT; + dropZoneView2.height = bounds2 != null ? bounds2.height() + halfDivider : MATCH_PARENT; + } else { + dropZoneView1.width = bounds1 != null ? bounds1.width() + halfDivider : MATCH_PARENT; + dropZoneView2.width = bounds2 != null ? bounds2.width() + halfDivider : MATCH_PARENT; + dropZoneView1.height = MATCH_PARENT; + dropZoneView2.height = MATCH_PARENT; + } + dropZoneView1.weight = bounds1 != null ? 0 : 1; + dropZoneView2.weight = bounds2 != null ? 0 : 1; + mDropZoneView1.setLayoutParams(dropZoneView1); + mDropZoneView2.setLayoutParams(dropZoneView2); } public void show() { @@ -137,20 +279,14 @@ public class DragLayout extends View { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); if (target == null) { // Animating to no target - mDropOutline.startVisibilityAnimation(false, LINEAR); - Rect finalBounds = new Rect(mCurrentTarget.drawRegion); - finalBounds.inset(mDisplayMargin, mDisplayMargin); - mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN); + animateSplitContainers(false, null /* animCompleteCallback */); } else if (mCurrentTarget == null) { // Animating to first target - mDropOutline.startVisibilityAnimation(true, LINEAR); - Rect initialBounds = new Rect(target.drawRegion); - initialBounds.inset(mDisplayMargin, mDisplayMargin); - mDropOutline.setRegionBounds(initialBounds); - mDropOutline.startBoundsAnimation(target.drawRegion, LINEAR_OUT_SLOW_IN); + animateSplitContainers(true, null /* animCompleteCallback */); + animateHighlight(target); } else { - // Bounds change - mDropOutline.startBoundsAnimation(target.drawRegion, FAST_OUT_SLOW_IN); + // Switching between targets + animateHighlight(target); } mCurrentTarget = target; } @@ -161,26 +297,7 @@ public class DragLayout extends View { */ public void hide(DragEvent event, Runnable hideCompleteCallback) { mIsShowing = false; - ObjectAnimator alphaAnimator = mDropOutline.startVisibilityAnimation(false, LINEAR); - ObjectAnimator boundsAnimator = null; - if (mCurrentTarget != null) { - Rect finalBounds = new Rect(mCurrentTarget.drawRegion); - finalBounds.inset(mDisplayMargin, mDisplayMargin); - boundsAnimator = mDropOutline.startBoundsAnimation(finalBounds, FAST_OUT_LINEAR_IN); - } - - if (hideCompleteCallback != null) { - ObjectAnimator lastAnim = boundsAnimator != null - ? boundsAnimator - : alphaAnimator; - lastAnim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - hideCompleteCallback.run(); - } - }); - } - + animateSplitContainers(false, hideCompleteCallback); mCurrentTarget = null; } @@ -199,4 +316,49 @@ public class DragLayout extends View { hide(event, dropCompleteCallback); return handledDrop; } + + private void animateSplitContainers(boolean visible, Runnable animCompleteCallback) { + mStatusBarManager.disable(visible + ? HIDE_STATUS_BAR_FLAGS + : DISABLE_NONE); + mDropZoneView1.setShowingMargin(visible); + mDropZoneView2.setShowingMargin(visible); + ObjectAnimator animator = mDropZoneView1.getAnimator(); + if (animCompleteCallback != null) { + if (animator != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + animCompleteCallback.run(); + } + }); + } else { + // If there's no animator the animation is done so run immediately + animCompleteCallback.run(); + } + } + } + + private void animateHighlight(DragAndDropPolicy.Target target) { + if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_LEFT + || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_TOP) { + mDropZoneView1.setShowingHighlight(true); + mDropZoneView1.setShowingSplash(false); + + mDropZoneView2.setShowingHighlight(false); + mDropZoneView2.setShowingSplash(true); + } else if (target.type == DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT + || target.type == DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM) { + mDropZoneView1.setShowingHighlight(false); + mDropZoneView1.setShowingSplash(true); + + mDropZoneView2.setShowingHighlight(true); + mDropZoneView2.setShowingSplash(false); + } + } + + private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) { + final int taskBgColor = taskInfo.taskDescription.getBackgroundColor(); + return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java index 64f7be5be813..73deea54e52f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropOutlineDrawable.java @@ -86,7 +86,7 @@ public class DropOutlineDrawable extends Drawable { public DropOutlineDrawable(Context context) { super(); // TODO(b/169894807): Use corner specific radii and maybe lower radius for non-edge corners - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context.getResources()); + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); mColor = context.getColor(R.color.drop_outline_background); mMaxAlpha = Color.alpha(mColor); // Initialize as hidden diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java new file mode 100644 index 000000000000..2f47af57d496 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DropZoneView.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2021 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.draganddrop; + +import static com.android.wm.shell.animation.Interpolators.FAST_OUT_SLOW_IN; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Path; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.IntProperty; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import androidx.annotation.Nullable; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.R; + +/** + * Renders a drop zone area for items being dragged. + */ +public class DropZoneView extends FrameLayout { + + private static final int SPLASHSCREEN_ALPHA_INT = (int) (255 * 0.90f); + private static final int HIGHLIGHT_ALPHA_INT = 255; + private static final int MARGIN_ANIMATION_ENTER_DURATION = 400; + private static final int MARGIN_ANIMATION_EXIT_DURATION = 250; + + private static final FloatProperty<DropZoneView> INSETS = + new FloatProperty<DropZoneView>("insets") { + @Override + public void setValue(DropZoneView v, float percent) { + v.setMarginPercent(percent); + } + + @Override + public Float get(DropZoneView v) { + return v.getMarginPercent(); + } + }; + + private static final IntProperty<ColorDrawable> SPLASHSCREEN_ALPHA = + new IntProperty<ColorDrawable>("splashscreen") { + @Override + public void setValue(ColorDrawable d, int alpha) { + d.setAlpha(alpha); + } + + @Override + public Integer get(ColorDrawable d) { + return d.getAlpha(); + } + }; + + private static final IntProperty<ColorDrawable> HIGHLIGHT_ALPHA = + new IntProperty<ColorDrawable>("highlight") { + @Override + public void setValue(ColorDrawable d, int alpha) { + d.setAlpha(alpha); + } + + @Override + public Integer get(ColorDrawable d) { + return d.getAlpha(); + } + }; + + private final Path mPath = new Path(); + private final float[] mContainerMargin = new float[4]; + private float mCornerRadius; + private float mBottomInset; + private int mMarginColor; // i.e. color used for negative space like the container insets + private int mHighlightColor; + + private boolean mShowingHighlight; + private boolean mShowingSplash; + private boolean mShowingMargin; + + // TODO: might be more seamless to animate between splash/highlight color instead of 2 separate + private ObjectAnimator mSplashAnimator; + private ObjectAnimator mHighlightAnimator; + private ObjectAnimator mMarginAnimator; + private float mMarginPercent; + + // Renders a highlight or neutral transparent color + private ColorDrawable mDropZoneDrawable; + // Renders the translucent splashscreen with the app icon in the middle + private ImageView mSplashScreenView; + private ColorDrawable mSplashBackgroundDrawable; + // Renders the margin / insets around the dropzone container + private MarginView mMarginView; + + public DropZoneView(Context context) { + this(context, null); + } + + public DropZoneView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public DropZoneView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setContainerMargin(0, 0, 0, 0); // make sure it's populated + + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); + mMarginColor = getResources().getColor(R.color.taskbar_background); + mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); + + mDropZoneDrawable = new ColorDrawable(); + mDropZoneDrawable.setColor(mHighlightColor); + mDropZoneDrawable.setAlpha(0); + setBackgroundDrawable(mDropZoneDrawable); + + mSplashScreenView = new ImageView(context); + mSplashScreenView.setScaleType(ImageView.ScaleType.CENTER); + mSplashBackgroundDrawable = new ColorDrawable(); + mSplashBackgroundDrawable.setColor(Color.WHITE); + mSplashBackgroundDrawable.setAlpha(SPLASHSCREEN_ALPHA_INT); + mSplashScreenView.setBackgroundDrawable(mSplashBackgroundDrawable); + addView(mSplashScreenView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + mSplashScreenView.setAlpha(0f); + + mMarginView = new MarginView(context); + addView(mMarginView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + + public void onThemeChange() { + mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(getContext()); + mMarginColor = getResources().getColor(R.color.taskbar_background); + mHighlightColor = getResources().getColor(android.R.color.system_accent1_500); + + final int alpha = mDropZoneDrawable.getAlpha(); + mDropZoneDrawable.setColor(mHighlightColor); + mDropZoneDrawable.setAlpha(alpha); + + if (mMarginPercent > 0) { + mMarginView.invalidate(); + } + } + + /** Sets the desired margins around the drop zone container when fully showing. */ + public void setContainerMargin(float left, float top, float right, float bottom) { + mContainerMargin[0] = left; + mContainerMargin[1] = top; + mContainerMargin[2] = right; + mContainerMargin[3] = bottom; + if (mMarginPercent > 0) { + mMarginView.invalidate(); + } + } + + /** Sets the bottom inset so the drop zones are above bottom navigation. */ + public void setBottomInset(float bottom) { + mBottomInset = bottom; + ((LayoutParams) mSplashScreenView.getLayoutParams()).bottomMargin = (int) bottom; + if (mMarginPercent > 0) { + mMarginView.invalidate(); + } + } + + /** Sets the color and icon to use for the splashscreen when shown. */ + public void setAppInfo(int splashScreenColor, Drawable appIcon) { + mSplashBackgroundDrawable.setColor(splashScreenColor); + mSplashScreenView.setImageDrawable(appIcon); + } + + /** @return an active animator for this view if one exists. */ + @Nullable + public ObjectAnimator getAnimator() { + if (mMarginAnimator != null && mMarginAnimator.isRunning()) { + return mMarginAnimator; + } else if (mHighlightAnimator != null && mHighlightAnimator.isRunning()) { + return mHighlightAnimator; + } else if (mSplashAnimator != null && mSplashAnimator.isRunning()) { + return mSplashAnimator; + } + return null; + } + + /** Animates the splashscreen to show or hide. */ + public void setShowingSplash(boolean showingSplash) { + if (mShowingSplash != showingSplash) { + mShowingSplash = showingSplash; + animateSplashToState(); + } + } + + /** Animates the highlight indicating the zone is hovered on or not. */ + public void setShowingHighlight(boolean showingHighlight) { + if (mShowingHighlight != showingHighlight) { + mShowingHighlight = showingHighlight; + animateHighlightToState(); + } + } + + /** Animates the margins around the drop zone to show or hide. */ + public void setShowingMargin(boolean visible) { + if (mShowingMargin != visible) { + mShowingMargin = visible; + animateMarginToState(); + } + if (!mShowingMargin) { + setShowingHighlight(false); + setShowingSplash(false); + } + } + + private void animateSplashToState() { + if (mSplashAnimator != null) { + mSplashAnimator.cancel(); + } + mSplashAnimator = ObjectAnimator.ofInt(mSplashBackgroundDrawable, + SPLASHSCREEN_ALPHA, + mSplashBackgroundDrawable.getAlpha(), + mShowingSplash ? SPLASHSCREEN_ALPHA_INT : 0); + if (!mShowingSplash) { + mSplashAnimator.setInterpolator(FAST_OUT_SLOW_IN); + } + mSplashAnimator.start(); + mSplashScreenView.animate().alpha(mShowingSplash ? 1f : 0f).start(); + } + + private void animateHighlightToState() { + if (mHighlightAnimator != null) { + mHighlightAnimator.cancel(); + } + mHighlightAnimator = ObjectAnimator.ofInt(mDropZoneDrawable, + HIGHLIGHT_ALPHA, + mDropZoneDrawable.getAlpha(), + mShowingHighlight ? HIGHLIGHT_ALPHA_INT : 0); + if (!mShowingHighlight) { + mHighlightAnimator.setInterpolator(FAST_OUT_SLOW_IN); + } + mHighlightAnimator.start(); + } + + private void animateMarginToState() { + if (mMarginAnimator != null) { + mMarginAnimator.cancel(); + } + mMarginAnimator = ObjectAnimator.ofFloat(this, INSETS, + mMarginPercent, + mShowingMargin ? 1f : 0f); + mMarginAnimator.setInterpolator(FAST_OUT_SLOW_IN); + mMarginAnimator.setDuration(mShowingMargin + ? MARGIN_ANIMATION_ENTER_DURATION + : MARGIN_ANIMATION_EXIT_DURATION); + mMarginAnimator.start(); + } + + private void setMarginPercent(float percent) { + if (percent != mMarginPercent) { + mMarginPercent = percent; + mMarginView.invalidate(); + } + } + + private float getMarginPercent() { + return mMarginPercent; + } + + /** Simple view that draws a rounded rect margin around its contents. **/ + private class MarginView extends View { + + MarginView(Context context) { + super(context); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + mPath.reset(); + mPath.addRoundRect(mContainerMargin[0] * mMarginPercent, + mContainerMargin[1] * mMarginPercent, + getWidth() - (mContainerMargin[2] * mMarginPercent), + getHeight() - (mContainerMargin[3] * mMarginPercent) - mBottomInset, + mCornerRadius * mMarginPercent, + mCornerRadius * mMarginPercent, + Path.Direction.CW); + mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD); + canvas.clipPath(mPath); + canvas.drawColor(mMarginColor); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java new file mode 100644 index 000000000000..52ff21bc3172 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2021 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.freeform; + +import static android.content.pm.PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT; +import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; + +import android.app.ActivityManager.RunningTaskInfo; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.provider.Settings; +import android.util.Slog; +import android.util.SparseArray; +import android.view.SurfaceControl; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; + +/** + * {@link ShellTaskOrganizer.TaskListener} for {@link + * ShellTaskOrganizer#TASK_LISTENER_TYPE_FREEFORM}. + */ +public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = "FreeformTaskListener"; + + private final SyncTransactionQueue mSyncQueue; + + private final SparseArray<State> mTasks = new SparseArray<>(); + + private static class State { + RunningTaskInfo mTaskInfo; + SurfaceControl mLeash; + } + + public FreeformTaskListener(SyncTransactionQueue syncQueue) { + mSyncQueue = syncQueue; + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mTasks.get(taskInfo.taskId) != null) { + throw new RuntimeException("Task appeared more than once: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Appeared: #%d", + taskInfo.taskId); + final State state = new State(); + state.mTaskInfo = taskInfo; + state.mLeash = leash; + mTasks.put(taskInfo.taskId, state); + + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + mSyncQueue.runInSync(t -> { + Point taskPosition = taskInfo.positionInParent; + t.setPosition(leash, taskPosition.x, taskPosition.y) + .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) + .show(leash); + }); + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + State state = mTasks.get(taskInfo.taskId); + if (state == null) { + Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + return; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Vanished: #%d", + taskInfo.taskId); + mTasks.remove(taskInfo.taskId); + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + State state = mTasks.get(taskInfo.taskId); + if (state == null) { + throw new RuntimeException( + "Task info changed before appearing: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Freeform Task Info Changed: #%d", + taskInfo.taskId); + state.mTaskInfo = taskInfo; + + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + final SurfaceControl leash = state.mLeash; + mSyncQueue.runInSync(t -> { + Point taskPosition = taskInfo.positionInParent; + t.setPosition(leash, taskPosition.x, taskPosition.y) + .setWindowCrop(leash, taskBounds.width(), taskBounds.height()) + .show(leash); + }); + } + + @Override + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + mTasks.size() + " tasks"); + } + + @Override + public String toString() { + return TAG; + } + + /** + * Checks if freeform support is enabled in system. + * + * @param context context used to check settings and package manager. + * @return {@code true} if freeform is enabled, {@code false} if not. + */ + public static boolean isFreeformEnabled(Context context) { + return context.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT) + || Settings.Global.getInt(context.getContentResolver(), + DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java new file mode 100644 index 000000000000..6e38e421d4b6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2020 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.fullscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; + +import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN; +import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.TaskInfo; +import android.graphics.Point; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.util.Optional; + +/** + * Organizes tasks presented in {@link android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN}. + */ +public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = "FullscreenTaskListener"; + + private final SyncTransactionQueue mSyncQueue; + private final FullscreenUnfoldController mFullscreenUnfoldController; + private final Optional<RecentTasksController> mRecentTasksOptional; + + private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>(); + private final AnimatableTasksListener mAnimatableTasksListener = new AnimatableTasksListener(); + + public FullscreenTaskListener(SyncTransactionQueue syncQueue, + Optional<FullscreenUnfoldController> unfoldController) { + this(syncQueue, unfoldController, Optional.empty()); + } + + public FullscreenTaskListener(SyncTransactionQueue syncQueue, + Optional<FullscreenUnfoldController> unfoldController, + Optional<RecentTasksController> recentTasks) { + mSyncQueue = syncQueue; + mFullscreenUnfoldController = unfoldController.orElse(null); + mRecentTasksOptional = recentTasks; + } + + @Override + public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mDataByTaskId.get(taskInfo.taskId) != null) { + throw new IllegalStateException("Task appeared more than once: #" + taskInfo.taskId); + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Appeared: #%d", + taskInfo.taskId); + final Point positionInParent = taskInfo.positionInParent; + mDataByTaskId.put(taskInfo.taskId, new TaskData(leash, positionInParent)); + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + mSyncQueue.runInSync(t -> { + // Reset several properties back to fullscreen (PiP, for example, leaves all these + // properties in a bad state). + t.setWindowCrop(leash, null); + t.setPosition(leash, positionInParent.x, positionInParent.y); + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + }); + + mAnimatableTasksListener.onTaskAppeared(taskInfo); + updateRecentsForVisibleFullscreenTask(taskInfo); + } + + @Override + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + if (Transitions.ENABLE_SHELL_TRANSITIONS) return; + + mAnimatableTasksListener.onTaskInfoChanged(taskInfo); + updateRecentsForVisibleFullscreenTask(taskInfo); + + final TaskData data = mDataByTaskId.get(taskInfo.taskId); + final Point positionInParent = taskInfo.positionInParent; + if (!positionInParent.equals(data.positionInParent)) { + data.positionInParent.set(positionInParent.x, positionInParent.y); + mSyncQueue.runInSync(t -> { + t.setPosition(data.surface, positionInParent.x, positionInParent.y); + }); + } + } + + @Override + public void onTaskVanished(RunningTaskInfo taskInfo) { + if (mDataByTaskId.get(taskInfo.taskId) == null) { + Slog.e(TAG, "Task already vanished: #" + taskInfo.taskId); + return; + } + + mAnimatableTasksListener.onTaskVanished(taskInfo); + mDataByTaskId.remove(taskInfo.taskId); + + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d", + taskInfo.taskId); + } + + private void updateRecentsForVisibleFullscreenTask(RunningTaskInfo taskInfo) { + mRecentTasksOptional.ifPresent(recentTasks -> { + if (taskInfo.isVisible) { + // Remove any persisted splits if either tasks are now made fullscreen and visible + recentTasks.removeSplitPair(taskInfo.taskId); + } + }); + } + + @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (!mDataByTaskId.contains(taskId)) { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + b.setParent(mDataByTaskId.get(taskId).surface); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + this); + pw.println(innerPrefix + mDataByTaskId.size() + " Tasks"); + } + + @Override + public String toString() { + return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_FULLSCREEN); + } + + /** + * Per-task data for each managed task. + */ + private static class TaskData { + public final SurfaceControl surface; + public final Point positionInParent; + + public TaskData(SurfaceControl surface, Point positionInParent) { + this.surface = surface; + this.positionInParent = positionInParent; + } + } + + class AnimatableTasksListener { + private final SparseBooleanArray mTaskIds = new SparseBooleanArray(); + + public void onTaskAppeared(RunningTaskInfo taskInfo) { + final boolean isApplicable = isAnimatable(taskInfo); + if (isApplicable) { + mTaskIds.put(taskInfo.taskId, true); + + if (mFullscreenUnfoldController != null) { + SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface; + mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash); + } + } + } + + public void onTaskInfoChanged(RunningTaskInfo taskInfo) { + final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId); + final boolean isApplicable = isAnimatable(taskInfo); + + if (isCurrentlyApplicable) { + if (isApplicable) { + // Still applicable, send update + if (mFullscreenUnfoldController != null) { + mFullscreenUnfoldController.onTaskInfoChanged(taskInfo); + } + } else { + // Became inapplicable + if (mFullscreenUnfoldController != null) { + mFullscreenUnfoldController.onTaskVanished(taskInfo); + } + mTaskIds.put(taskInfo.taskId, false); + } + } else { + if (isApplicable) { + // Became applicable + mTaskIds.put(taskInfo.taskId, true); + + if (mFullscreenUnfoldController != null) { + SurfaceControl leash = mDataByTaskId.get(taskInfo.taskId).surface; + mFullscreenUnfoldController.onTaskAppeared(taskInfo, leash); + } + } + } + } + + public void onTaskVanished(RunningTaskInfo taskInfo) { + final boolean isCurrentlyApplicable = mTaskIds.get(taskInfo.taskId); + if (isCurrentlyApplicable && mFullscreenUnfoldController != null) { + mFullscreenUnfoldController.onTaskVanished(taskInfo); + } + mTaskIds.put(taskInfo.taskId, false); + } + + private boolean isAnimatable(TaskInfo taskInfo) { + // Filter all visible tasks that are not launcher tasks + // We do not animate launcher as it handles the animation by itself + return taskInfo != null && taskInfo.isVisible && taskInfo.getConfiguration() + .windowConfiguration.getActivityType() != ACTIVITY_TYPE_HOME; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java new file mode 100644 index 000000000000..aa3868cfca84 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenUnfoldController.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2021 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.fullscreen; + +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.util.MathUtils.lerp; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.app.TaskInfo; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.concurrent.Executor; + +/** + * Controls full screen app unfold transition: animating cropping window and scaling when + * folding or unfolding a foldable device. + */ +public final class FullscreenUnfoldController implements UnfoldListener, + OnInsetsChangedListener { + + private static final float[] FLOAT_9 = new float[9]; + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + + private static final float HORIZONTAL_START_MARGIN = 0.08f; + private static final float VERTICAL_START_MARGIN = 0.03f; + private static final float END_SCALE = 1f; + private static final float START_SCALE = END_SCALE - VERTICAL_START_MARGIN * 2; + + private final Executor mExecutor; + private final ShellUnfoldProgressProvider mProgressProvider; + private final DisplayInsetsController mDisplayInsetsController; + + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final UnfoldBackgroundController mBackgroundController; + + private InsetsSource mTaskbarInsetsSource; + + private final float mWindowCornerRadiusPx; + private final float mExpandedTaskBarHeight; + + private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); + + public FullscreenUnfoldController( + @NonNull Context context, + @NonNull Executor executor, + @NonNull UnfoldBackgroundController backgroundController, + @NonNull ShellUnfoldProgressProvider progressProvider, + @NonNull DisplayInsetsController displayInsetsController + ) { + mExecutor = executor; + mProgressProvider = progressProvider; + mDisplayInsetsController = displayInsetsController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + mBackgroundController = backgroundController; + } + + /** + * Initializes the controller + */ + public void init() { + mProgressProvider.addListener(mExecutor, this); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + } + + @Override + public void onStateChangeProgress(float progress) { + if (mAnimationContextByTaskId.size() == 0) return; + + mBackgroundController.ensureBackground(mTransaction); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + + context.mCurrentCropRect.set(RECT_EVALUATOR + .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); + + float scale = lerp(START_SCALE, END_SCALE, progress); + context.mMatrix.setScale(scale, scale, context.mCurrentCropRect.exactCenterX(), + context.mCurrentCropRect.exactCenterY()); + + mTransaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setMatrix(context.mLeash, context.mMatrix, FLOAT_9) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + + mTransaction.apply(); + } + + @Override + public void onStateChangeFinished() { + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(context); + } + + mBackgroundController.removeBackground(mTransaction); + mTransaction.apply(); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(mTaskbarInsetsSource, context.mTaskInfo); + } + } + + /** + * Called when a new matching task appeared + */ + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + AnimationContext animationContext = new AnimationContext(leash, mTaskbarInsetsSource, + taskInfo); + mAnimationContextByTaskId.put(taskInfo.taskId, animationContext); + } + + /** + * Called when matching task changed + */ + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); + if (animationContext != null) { + animationContext.update(mTaskbarInsetsSource, taskInfo); + } + } + + /** + * Called when matching task vanished + */ + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext animationContext = mAnimationContextByTaskId.get(taskInfo.taskId); + if (animationContext != null) { + // PiP task has its own cleanup path, ignore surface reset to avoid conflict. + if (taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED) { + resetSurface(animationContext); + } + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + if (mAnimationContextByTaskId.size() == 0) { + mBackgroundController.removeBackground(mTransaction); + } + + mTransaction.apply(); + } + + private void resetSurface(AnimationContext context) { + mTransaction + .setWindowCrop(context.mLeash, null) + .setCornerRadius(context.mLeash, 0.0F) + .setMatrix(context.mLeash, 1.0F, 0.0F, 0.0F, 1.0F) + .setPosition(context.mLeash, + (float) context.mTaskInfo.positionInParent.x, + (float) context.mTaskInfo.positionInParent.y); + } + + private class AnimationContext { + final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); + final Rect mEndCropRect = new Rect(); + final Rect mCurrentCropRect = new Rect(); + final Matrix mMatrix = new Matrix(); + + TaskInfo mTaskInfo; + + private AnimationContext(SurfaceControl leash, + InsetsSource taskBarInsetsSource, + TaskInfo taskInfo) { + this.mLeash = leash; + update(taskBarInsetsSource, taskInfo); + } + + private void update(InsetsSource taskBarInsetsSource, TaskInfo taskInfo) { + mTaskInfo = taskInfo; + mStartCropRect.set(mTaskInfo.getConfiguration().windowConfiguration.getBounds()); + + if (taskBarInsetsSource != null) { + // Only insets the cropping window with task bar when it's expanded + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mStartCropRect.inset(taskBarInsetsSource + .calculateVisibleInsets(mStartCropRect)); + } + } + + mEndCropRect.set(mStartCropRect); + + int horizontalMargin = (int) (mEndCropRect.width() * HORIZONTAL_START_MARGIN); + mStartCropRect.left = mEndCropRect.left + horizontalMargin; + mStartCropRect.right = mEndCropRect.right - horizontalMargin; + int verticalMargin = (int) (mEndCropRect.height() * VERTICAL_START_MARGIN); + mStartCropRect.top = mEndCropRect.top + verticalMargin; + mStartCropRect.bottom = mEndCropRect.bottom - verticalMargin; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java index 75a1ddeccb22..3f7d78dda037 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizer.java @@ -39,7 +39,7 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.internal.R; +import com.android.internal.policy.SystemBarUtils; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -307,12 +307,9 @@ class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer { t.apply(); } - private int getStatusBarHeight() { - final boolean isLandscape = - mIsDefaultPortrait ? isDisplaySizeFlipped() : !isDisplaySizeFlipped(); - return mContext.getResources().getDimensionPixelSize( - isLandscape ? R.dimen.status_bar_height_landscape - : R.dimen.status_bar_height_portrait); + @VisibleForTesting + int getStatusBarHeight() { + return SystemBarUtils.getStatusBarHeight(mContext); } void dump(@NonNull PrintWriter pw) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java index 362b40f33e89..067f80800ed5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/DividerView.java @@ -20,6 +20,8 @@ import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW; import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW; import static android.view.WindowManager.DOCKED_RIGHT; +import static com.android.wm.shell.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import static com.android.wm.shell.common.split.DividerView.TOUCH_ANIMATION_DURATION; import static com.android.wm.shell.common.split.DividerView.TOUCH_RELEASE_ANIMATION_DURATION; @@ -100,10 +102,6 @@ public class DividerView extends FrameLayout implements OnTouchListener, private static final float MINIMIZE_DOCK_SCALE = 0f; private static final float ADJUSTED_FOR_IME_SCALE = 0.5f; - private static final PathInterpolator SLOWDOWN_INTERPOLATOR = - new PathInterpolator(0.5f, 1f, 0.5f, 1f); - private static final PathInterpolator DIM_INTERPOLATOR = - new PathInterpolator(.23f, .87f, .52f, -0.11f); private static final Interpolator IME_ADJUST_INTERPOLATOR = new PathInterpolator(0.2f, 0f, 0.1f, 1f); @@ -460,6 +458,7 @@ public class DividerView extends FrameLayout implements OnTouchListener, private void stopDragging() { mHandle.setTouching(false, true /* animate */); mWindowManager.setSlippery(true); + mWindowManagerProxy.setResizing(false); releaseBackground(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java index 40244fbb4503..f201634d3d4a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitDisplayLayout.java @@ -62,6 +62,7 @@ public class LegacySplitDisplayLayout { Rect mSecondary = null; Rect mAdjustedPrimary = null; Rect mAdjustedSecondary = null; + final Rect mTmpBounds = new Rect(); public LegacySplitDisplayLayout(Context ctx, DisplayLayout dl, LegacySplitScreenTaskListener taskTiles) { @@ -136,31 +137,41 @@ public class LegacySplitDisplayLayout { return mMinimizedSnapAlgorithm; } - void resizeSplits(int position) { + /** + * Resize primary bounds and secondary bounds by divider position. + * + * @param position divider position. + * @return true if calculated bounds changed. + */ + boolean resizeSplits(int position) { mPrimary = mPrimary == null ? new Rect() : mPrimary; mSecondary = mSecondary == null ? new Rect() : mSecondary; - calcSplitBounds(position, mPrimary, mSecondary); - } - - void resizeSplits(int position, WindowContainerTransaction t) { - resizeSplits(position); - t.setBounds(mTiles.mPrimary.token, mPrimary); - t.setBounds(mTiles.mSecondary.token, mSecondary); - - t.setSmallestScreenWidthDp(mTiles.mPrimary.token, - getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); - t.setSmallestScreenWidthDp(mTiles.mSecondary.token, - getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); - } - - void calcSplitBounds(int position, @NonNull Rect outPrimary, @NonNull Rect outSecondary) { int dockSide = getPrimarySplitSide(); - DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outPrimary, + boolean boundsChanged; + + mTmpBounds.set(mPrimary); + DockedDividerUtils.calculateBoundsForPosition(position, dockSide, mPrimary, mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); + boundsChanged = !mPrimary.equals(mTmpBounds); + mTmpBounds.set(mSecondary); DockedDividerUtils.calculateBoundsForPosition(position, - DockedDividerUtils.invertDockSide(dockSide), outSecondary, mDisplayLayout.width(), + DockedDividerUtils.invertDockSide(dockSide), mSecondary, mDisplayLayout.width(), mDisplayLayout.height(), mDividerSize); + boundsChanged |= !mSecondary.equals(mTmpBounds); + return boundsChanged; + } + + void resizeSplits(int position, WindowContainerTransaction t) { + if (resizeSplits(position)) { + t.setBounds(mTiles.mPrimary.token, mPrimary); + t.setBounds(mTiles.mSecondary.token, mSecondary); + + t.setSmallestScreenWidthDp(mTiles.mPrimary.token, + getSmallestWidthDpForBounds(mContext, mDisplayLayout, mPrimary)); + t.setSmallestScreenWidthDp(mTiles.mSecondary.token, + getSmallestWidthDpForBounds(mContext, mDisplayLayout, mSecondary)); + } } Rect calcResizableMinimizedHomeStackBounds() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java index 80ab166d0649..67e487de0993 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenController.java @@ -172,6 +172,14 @@ public class LegacySplitScreenController implements DisplayController.OnDisplays }; mWindowManager = new DividerWindowManager(mSystemWindows); + + // No need to listen to display window container or create root tasks if the device is not + // using legacy split screen. + if (!context.getResources().getBoolean(com.android.internal.R.bool.config_useLegacySplit)) { + return; + } + + mDisplayController.addDisplayWindowListener(this); // Don't initialize the divider or anything until we get the default display. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java index d9409ec2dc17..b1fa2ac25fe7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/legacysplitscreen/LegacySplitScreenTransitions.java @@ -204,7 +204,8 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition != mPendingDismiss && transition != mPendingEnter) { // If we're not in split-mode, just abort @@ -239,12 +240,12 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl if (change.getParent() != null) { // This is probably reparented, so we want the parent to be immediately visible final TransitionInfo.Change parentChange = info.getChange(change.getParent()); - t.show(parentChange.getLeash()); - t.setAlpha(parentChange.getLeash(), 1.f); + startTransaction.show(parentChange.getLeash()); + startTransaction.setAlpha(parentChange.getLeash(), 1.f); // and then animate this layer outside the parent (since, for example, this is // the home task animating from fullscreen to part-screen). - t.reparent(leash, info.getRootLeash()); - t.setLayer(leash, info.getChanges().size() - i); + startTransaction.reparent(leash, info.getRootLeash()); + startTransaction.setLayer(leash, info.getChanges().size() - i); // build the finish reparent/reposition mFinishTransaction.reparent(leash, parentChange.getLeash()); mFinishTransaction.setPosition(leash, @@ -271,12 +272,12 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl if (transition == mPendingEnter && mListener.mPrimary.token.equals(change.getContainer()) || mListener.mSecondary.token.equals(change.getContainer())) { - t.setWindowCrop(leash, change.getStartAbsBounds().width(), + startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(), change.getStartAbsBounds().height()); if (mListener.mPrimary.token.equals(change.getContainer())) { // Move layer to top since we want it above the oversized home task during // animation even though home task is on top in hierarchy. - t.setLayer(leash, info.getChanges().size() + 1); + startTransaction.setLayer(leash, info.getChanges().size() + 1); } } boolean isOpening = Transitions.isOpeningType(info.getType()); @@ -289,7 +290,7 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl // Dismissing via snap-to-top/bottom means that the dismissed task is already // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 // and don't animate it so it doesn't pop-in when reparented. - t.setAlpha(leash, 0.f); + startTransaction.setAlpha(leash, 0.f); } else { startExampleAnimation(leash, false /* show */); } @@ -311,7 +312,7 @@ public class LegacySplitScreenTransitions implements Transitions.TransitionHandl } mSplitScreen.finishEnterSplitTransition(homeIsVisible); } - t.apply(); + startTransaction.apply(); onFinish(); return true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OWNERS new file mode 100644 index 000000000000..41177f01b208 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module one handed mode owner +lbill@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java index e511bffad247..e0686146e821 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java @@ -43,9 +43,9 @@ import android.util.Slog; import android.view.Surface; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.logging.UiEventLogger; @@ -64,7 +64,8 @@ import java.io.PrintWriter; /** * Manages and manipulates the one handed states, transitions, and gesture for phones. */ -public class OneHandedController implements RemoteCallable<OneHandedController> { +public class OneHandedController implements RemoteCallable<OneHandedController>, + DisplayChangeController.OnDisplayChangingListener { private static final String TAG = "OneHandedController"; private static final String ONE_HANDED_MODE_OFFSET_PERCENTAGE = @@ -74,7 +75,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController> private static final int OVERLAY_ENABLED_DELAY_MS = 250; private static final int DISPLAY_AREA_READY_RETRY_MS = 10; - static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; + public static final String SUPPORT_ONE_HANDED_MODE = "ro.support_one_handed_mode"; private volatile boolean mIsOneHandedEnabled; private volatile boolean mIsSwipeToNotificationEnabled; @@ -106,19 +107,6 @@ public class OneHandedController implements RemoteCallable<OneHandedController> private OneHandedBackgroundPanelOrganizer mBackgroundPanelOrganizer; private OneHandedUiEventLogger mOneHandedUiEventLogger; - /** - * Handle rotation based on OnDisplayChangingListener callback - */ - private final DisplayChangeController.OnDisplayChangingListener mRotationController = - (display, fromRotation, toRotation, wct) -> { - if (!isInitialized()) { - return; - } - mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct); - mOneHandedUiEventLogger.writeEvent( - OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); - }; - private final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = new DisplayController.OnDisplaysChangedListener() { @Override @@ -209,16 +197,10 @@ public class OneHandedController implements RemoteCallable<OneHandedController> /** * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported. */ - @Nullable public static OneHandedController create( Context context, WindowManager windowManager, DisplayController displayController, DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener, UiEventLogger uiEventLogger, ShellExecutor mainExecutor, Handler mainHandler) { - if (!SystemProperties.getBoolean(SUPPORT_ONE_HANDED_MODE, false)) { - Slog.w(TAG, "Device doesn't support OneHanded feature"); - return null; - } - OneHandedSettingsUtil settingsUtil = new OneHandedSettingsUtil(); OneHandedAccessibilityUtil accessibilityUtil = new OneHandedAccessibilityUtil(context); OneHandedTimeoutHandler timeoutHandler = new OneHandedTimeoutHandler(mainExecutor); @@ -296,7 +278,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController> getObserver(this::onSwipeToNotificationEnabledChanged); mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged); - mDisplayController.addDisplayChangingController(mRotationController); + mDisplayController.addDisplayChangingController(this); setupCallback(); registerSettingObservers(mUserId); setupTimeoutListener(); @@ -548,6 +530,7 @@ public class OneHandedController implements RemoteCallable<OneHandedController> mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( mContext.getContentResolver(), mUserId); setSwipeToNotificationEnabled(enabled); + notifyShortcutStateChanged(mState.getState()); mOneHandedUiEventLogger.writeEvent(enabled ? OneHandedUiEventLogger.EVENT_ONE_HANDED_SETTINGS_SHOW_NOTIFICATION_ENABLED_ON @@ -699,6 +682,8 @@ public class OneHandedController implements RemoteCallable<OneHandedController> pw.println(mUserId); pw.print(innerPrefix + "isShortcutEnabled="); pw.println(isShortcutEnabled()); + pw.print(innerPrefix + "mIsSwipeToNotificationEnabled="); + pw.println(mIsSwipeToNotificationEnabled); if (mBackgroundPanelOrganizer != null) { mBackgroundPanelOrganizer.dump(pw); @@ -745,6 +730,27 @@ public class OneHandedController implements RemoteCallable<OneHandedController> } /** + * Handles rotation based on OnDisplayChangingListener callback + */ + @Override + public void onRotateDisplay(int displayId, int fromRotation, int toRotation, + WindowContainerTransaction wct) { + if (!isInitialized()) { + return; + } + + if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(mContext.getContentResolver(), + mUserId) || mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( + mContext.getContentResolver(), mUserId)) { + return; + } + + mDisplayAreaOrganizer.onRotateDisplay(mContext, toRotation, wct); + mOneHandedUiEventLogger.writeEvent( + OneHandedUiEventLogger.EVENT_ONE_HANDED_TRIGGER_ROTATION_OUT); + } + + /** * The interface for calls from outside the Shell, within the host process. */ @ExternalThread diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java index c2bbd9e99bac..1b2f4768110b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedDisplayAreaOrganizer.java @@ -16,8 +16,6 @@ package com.android.wm.shell.onehanded; -import static android.os.UserHandle.myUserId; - import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_EXIT; import static com.android.wm.shell.onehanded.OneHandedAnimationController.TRANSITION_DIRECTION_TRIGGER; @@ -186,20 +184,8 @@ public class OneHandedDisplayAreaOrganizer extends DisplayAreaOrganizer { if (mDisplayLayout.rotation() == toRotation) { return; } - - if (!mOneHandedSettingsUtil.getSettingsOneHandedModeEnabled(context.getContentResolver(), - myUserId())) { - return; - } - mDisplayLayout.rotateTo(context.getResources(), toRotation); updateDisplayBounds(); - - if (mOneHandedSettingsUtil.getSettingsSwipeToNotificationEnabled( - context.getContentResolver(), myUserId())) { - // If current settings is swipe notification, skip finishOffset. - return; - } finishOffset(0, TRANSITION_DIRECTION_EXIT); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java index ff333c8c659d..2cb7d1b0fa0d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedSettingsUtil.java @@ -244,6 +244,8 @@ public final class OneHandedSettingsUtil { pw.println(TAG); pw.print(innerPrefix + "isOneHandedModeEnable="); pw.println(getSettingsOneHandedModeEnabled(resolver, userId)); + pw.print(innerPrefix + "isSwipeToNotificationEnabled="); + pw.println(getSettingsSwipeToNotificationEnabled(resolver, userId)); pw.print(innerPrefix + "oneHandedTimeOut="); pw.println(getSettingsOneHandedModeTimeout(resolver, userId)); pw.print(innerPrefix + "tapsAppToExit="); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS new file mode 100644 index 000000000000..afddfab99a2b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-module pip owner +hwwang@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index 6d4773bdeb1f..c0734e95ecb7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -112,11 +112,17 @@ public interface Pip { default void showPictureInPictureMenu() {} /** - * Called by NavigationBar in order to listen in for PiP bounds change. This is mostly used - * for times where the PiP bounds could conflict with SystemUI elements, such as a stashed - * PiP and the Back-from-Edge gesture. + * Called by NavigationBar and TaskbarDelegate in order to listen in for PiP bounds change. This + * is mostly used for times where the PiP bounds could conflict with SystemUI elements, such as + * a stashed PiP and the Back-from-Edge gesture. */ - default void setPipExclusionBoundsChangeListener(Consumer<Rect> listener) { } + default void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { } + + /** + * Remove a callback added previously. This is used when NavigationBar is removed from the + * view hierarchy or destroyed. + */ + default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { } /** * Dump the current state and information if need. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 200af7415eb1..9575b0a720bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -38,6 +38,7 @@ import android.view.SurfaceSession; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.transition.Transitions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -593,10 +594,10 @@ public class PipAnimationController { getSurfaceTransactionHelper().scaleAndCrop(tx, leash, initialSourceValue, bounds, insets); if (shouldApplyCornerRadius()) { - final Rect destinationBounds = new Rect(bounds); - destinationBounds.inset(insets); + final Rect sourceBounds = new Rect(initialContainerRect); + sourceBounds.inset(insets); getSurfaceTransactionHelper().round(tx, leash, - initialContainerRect, destinationBounds); + sourceBounds, bounds); } } if (!handlePipTransaction(leash, tx, bounds)) { @@ -617,20 +618,36 @@ public class PipAnimationController { setCurrentValue(bounds); final Rect insets = computeInsets(fraction); final float degree, x, y; - if (rotationDelta == ROTATION_90) { - degree = 90 * fraction; - x = fraction * (end.right - start.left) + start.left; - y = fraction * (end.top - start.top) + start.top; + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + if (rotationDelta == ROTATION_90) { + degree = 90 * (1 - fraction); + x = fraction * (end.left - start.left) + + start.left + start.right * (1 - fraction); + y = fraction * (end.top - start.top) + start.top; + } else { + degree = -90 * (1 - fraction); + x = fraction * (end.left - start.left) + start.left; + y = fraction * (end.top - start.top) + + start.top + start.bottom * (1 - fraction); + } } else { - degree = -90 * fraction; - x = fraction * (end.left - start.left) + start.left; - y = fraction * (end.bottom - start.top) + start.top; + if (rotationDelta == ROTATION_90) { + degree = 90 * fraction; + x = fraction * (end.right - start.left) + start.left; + y = fraction * (end.top - start.top) + start.top; + } else { + degree = -90 * fraction; + x = fraction * (end.left - start.left) + start.left; + y = fraction * (end.bottom - start.top) + start.top; + } } + final Rect sourceBounds = new Rect(initialContainerRect); + sourceBounds.inset(insets); getSurfaceTransactionHelper() .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, rotationDelta == ROTATION_270 /* clockwise */) - .round(tx, leash, initialContainerRect, bounds); + .round(tx, leash, sourceBounds, bounds); tx.apply(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java index e3674dc920d5..b3558ad4b91e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java @@ -38,6 +38,8 @@ import com.android.wm.shell.common.DisplayLayout; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; @@ -89,7 +91,7 @@ public final class PipBoundsState { private @Nullable Runnable mOnMinimalSizeChangeCallback; private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; - private @Nullable Consumer<Rect> mOnPipExclusionBoundsChangeCallback; + private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>(); public PipBoundsState(@NonNull Context context) { mContext = context; @@ -108,8 +110,8 @@ public final class PipBoundsState { /** Set the current PIP bounds. */ public void setBounds(@NonNull Rect bounds) { mBounds.set(bounds); - if (mOnPipExclusionBoundsChangeCallback != null) { - mOnPipExclusionBoundsChangeCallback.accept(bounds); + for (Consumer<Rect> callback : mOnPipExclusionBoundsChangeCallbacks) { + callback.accept(bounds); } } @@ -407,17 +409,25 @@ public final class PipBoundsState { } /** - * Set a callback to watch out for PiP bounds. This is mostly used by SystemUI's + * Add a callback to watch out for PiP bounds. This is mostly used by SystemUI's * Back-gesture handler, to avoid conflicting with PiP when it's stashed. */ - public void setPipExclusionBoundsChangeCallback( + public void addPipExclusionBoundsChangeCallback( @Nullable Consumer<Rect> onPipExclusionBoundsChangeCallback) { - mOnPipExclusionBoundsChangeCallback = onPipExclusionBoundsChangeCallback; - if (mOnPipExclusionBoundsChangeCallback != null) { - mOnPipExclusionBoundsChangeCallback.accept(getBounds()); + mOnPipExclusionBoundsChangeCallbacks.add(onPipExclusionBoundsChangeCallback); + for (Consumer<Rect> callback : mOnPipExclusionBoundsChangeCallbacks) { + callback.accept(getBounds()); } } + /** + * Remove a callback that was previously added. + */ + public void removePipExclusionBoundsChangeCallback( + @Nullable Consumer<Rect> onPipExclusionBoundsChangeCallback) { + mOnPipExclusionBoundsChangeCallbacks.remove(onPipExclusionBoundsChangeCallback); + } + /** Source of truth for the current bounds of PIP that may be in motion. */ public static class MotionBoundsState { /** The bounds used when PIP is in motion (e.g. during a drag or animation) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java index 8d9ad4d1b96c..caa1f017082b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java @@ -90,6 +90,11 @@ public interface PipMenuController { default void updateMenuBounds(Rect destinationBounds) {} /** + * Update when the current focused task changes. + */ + default void onFocusTaskChanged(RunningTaskInfo taskInfo) {} + + /** * Returns a default LayoutParams for the PIP Menu. * @param width the PIP stack width. * @param height the PIP stack height. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index 728794de0865..180e3fb48c9d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -23,6 +23,7 @@ import android.graphics.RectF; import android.view.SurfaceControl; import com.android.wm.shell.R; +import com.android.wm.shell.transition.Transitions; /** * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. @@ -137,7 +138,8 @@ public class PipSurfaceTransactionHelper { // destination are different. final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH; final Rect crop = mTmpDestinationRect; - crop.set(0, 0, destW, destH); + crop.set(0, 0, Transitions.ENABLE_SHELL_TRANSITIONS ? destH + : destW, Transitions.ENABLE_SHELL_TRANSITIONS ? destW : destH); // Inverse scale for crop to fit in screen coordinates. crop.scale(1 / scale); crop.offset(insets.left, insets.top); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index f2bad6caf3e8..f0b2716f05d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -77,6 +77,7 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PipMotionHelper; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; @@ -97,7 +98,7 @@ import java.util.function.IntConsumer; * see also {@link PipMotionHelper}. */ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, - DisplayController.OnDisplaysChangedListener { + DisplayController.OnDisplaysChangedListener, ShellTaskOrganizer.FocusListener { private static final String TAG = PipTaskOrganizer.class.getSimpleName(); private static final boolean DEBUG = false; /** @@ -114,38 +115,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500; - // Not a complete set of states but serves what we want right now. - private enum State { - UNDEFINED(0), - TASK_APPEARED(1), - ENTRY_SCHEDULED(2), - ENTERING_PIP(3), - ENTERED_PIP(4), - EXITING_PIP(5); - - private final int mStateValue; - - State(int value) { - mStateValue = value; - } - - private boolean isInPip() { - return mStateValue >= TASK_APPEARED.mStateValue - && mStateValue != EXITING_PIP.mStateValue; - } - - /** - * Resize request can be initiated in other component, ignore if we are no longer in PIP, - * still waiting for animation or we're exiting from it. - * - * @return {@code true} if the resize request should be blocked/ignored. - */ - private boolean shouldBlockResizeRequest() { - return mStateValue < ENTERING_PIP.mStateValue - || mStateValue == EXITING_PIP.mStateValue; - } - } - private final Context mContext; private final SyncTransactionQueue mSyncTransactionQueue; private final PipBoundsState mPipBoundsState; @@ -158,7 +127,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private final int mExitAnimationDuration; private final int mCrossFadeAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private final Optional<LegacySplitScreenController> mSplitScreenOptional; + private final Optional<LegacySplitScreenController> mLegacySplitScreenOptional; + private final Optional<SplitScreenController> mSplitScreenOptional; protected final ShellTaskOrganizer mTaskOrganizer; protected final ShellExecutor mMainExecutor; @@ -169,11 +139,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void onPipAnimationStart(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // TODO (b//169221267): Add jank listener for transactions without buffer updates. - //InteractionJankMonitor.getInstance().begin( - // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); - } sendOnPipTransitionStarted(direction); } @@ -201,7 +166,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } final boolean isExitPipDirection = isOutPipDirection(direction) || isRemovePipDirection(direction); - if (mState != State.EXITING_PIP || isExitPipDirection) { + if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP + || isExitPipDirection) { // Finish resize as long as we're not exiting PIP, or, if we are, only if this is // the end of an exit PIP animation. // This is necessary in case there was a resize animation ongoing when exit PIP @@ -244,7 +210,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private ActivityManager.RunningTaskInfo mDeferredTaskInfo; private WindowContainerToken mToken; private SurfaceControl mLeash; - private State mState = State.UNDEFINED; + private PipTransitionState mPipTransitionState; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private long mLastOneShotAlphaAnimationTime; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory @@ -274,34 +240,29 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private @Surface.Rotation int mCurrentRotation; /** - * If set to {@code true}, no entering PiP transition would be kicked off and most likely - * it's due to the fact that Launcher is handling the transition directly when swiping - * auto PiP-able Activity to home. - * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}. - */ - private boolean mInSwipePipToHomeTransition; - - /** * An optional overlay used to mask content changing between an app in/out of PiP, only set if - * {@link #mInSwipePipToHomeTransition} is true. + * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true. */ private SurfaceControl mSwipePipToHomeOverlay; public PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, + @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, - Optional<LegacySplitScreenController> splitScreenOptional, + Optional<LegacySplitScreenController> legacySplitScreenOptional, + Optional<SplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { mContext = context; mSyncTransactionQueue = syncTransactionQueue; + mPipTransitionState = pipTransitionState; mPipBoundsState = pipBoundsState; mPipBoundsAlgorithm = boundsHandler; mPipMenuController = pipMenuController; @@ -316,6 +277,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController = pipAnimationController; mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; + mLegacySplitScreenOptional = legacySplitScreenOptional; mSplitScreenOptional = splitScreenOptional; mTaskOrganizer = shellTaskOrganizer; mMainExecutor = mainExecutor; @@ -324,6 +286,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mMainExecutor.execute(() -> { mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); }); + mTaskOrganizer.addFocusListener(this); + mPipTransitionController.setPipOrganizer(this); displayController.addDisplayWindowListener(this); } @@ -337,14 +301,14 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } public boolean isInPip() { - return mState.isInPip(); + return mPipTransitionState.isInPip(); } /** * Returns whether the entry animation is waiting to be started. */ public boolean isEntryScheduled() { - return mState == State.ENTRY_SCHEDULED; + return mPipTransitionState.getTransitionState() == PipTransitionState.ENTRY_SCHEDULED; } /** @@ -372,7 +336,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams) { - mInSwipePipToHomeTransition = true; + mPipTransitionState.setInSwipePipToHomeTransition(true); sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); return mPipBoundsAlgorithm.getEntryDestinationBounds(); @@ -385,12 +349,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, SurfaceControl overlay) { // do nothing if there is no startSwipePipToHome being called before - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { mPipBoundsState.setBounds(destinationBounds); mSwipePipToHomeOverlay = overlay; } } + public ActivityManager.RunningTaskInfo getTaskInfo() { + return mTaskInfo; + } + public SurfaceControl getSurfaceControl() { return mLeash; } @@ -410,11 +378,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * activity render it's final configuration while the Task is still in PiP. * - setWindowingMode to undefined at the end of transition * @param animationDurationMs duration in millisecond for the exiting PiP transition + * @param requestEnterSplit whether the enterSplit button is pressed on PiP or not. + * Indicate the user wishes to directly put PiP into split screen + * mode. */ - public void exitPip(int animationDurationMs) { - if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) { + public void exitPip(int animationDurationMs, boolean requestEnterSplit) { + if (!mPipTransitionState.isInPip() + || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP + || mToken == null) { Log.wtf(TAG, "Not allowed to exitPip in current state" - + " mState=" + mState + " mToken=" + mToken); + + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken); return; } @@ -422,7 +395,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); final WindowContainerTransaction wct = new WindowContainerTransaction(); final Rect destinationBounds = mPipBoundsState.getDisplayBounds(); - final int direction = syncWithSplitScreenBounds(destinationBounds) + final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit) ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_LEAVE_PIP; final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); @@ -431,14 +404,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // We set to fullscreen here for now, but later it will be set to UNDEFINED for // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. wct.setActivityWindowingMode(mToken, - direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN + direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN && !requestEnterSplit ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY : WINDOWING_MODE_FULLSCREEN); wct.setBounds(mToken, destinationBounds); wct.setBoundsChangeTransaction(mToken, tx); // Set the exiting state first so if there is fixed rotation later, the running animation // won't be interrupted by alpha animation for existing PiP. - mState = State.EXITING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); + + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mPipTransitionController.startTransition(destinationBounds, wct); + return; + } mSyncTransactionQueue.queue(wct); mSyncTransactionQueue.runInSync(t -> { // Make sure to grab the latest source hint rect as it could have been @@ -465,7 +443,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, wct.setWindowingMode(mToken, getOutPipWindowingMode()); // Simply reset the activity mode set prior to the animation running. wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); - mSplitScreenOptional.ifPresent(splitScreen -> { + mLegacySplitScreenOptional.ifPresent(splitScreen -> { if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */); } @@ -476,9 +454,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * Removes PiP immediately. */ public void removePip() { - if (!mState.isInPip() || mToken == null) { + if (!mPipTransitionState.isInPip() || mToken == null) { Log.wtf(TAG, "Not allowed to removePip in current state" - + " mState=" + mState + " mToken=" + mToken); + + " mState=" + mPipTransitionState.getTransitionState() + " mToken=" + mToken); return; } @@ -492,10 +470,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, animator.setDuration(mExitAnimationDuration); animator.setInterpolator(Interpolators.ALPHA_OUT); animator.start(); - mState = State.EXITING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); } private void removePipImmediately() { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mToken, null); + wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + wct.reorder(mToken, false); + mPipTransitionController.startTransition(null, wct); + return; + } + try { // Reset the task bounds first to ensure the activity configuration is reset as well final WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -514,7 +501,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Objects.requireNonNull(info, "Requires RunningTaskInfo"); mTaskInfo = info; mToken = mTaskInfo.token; - mState = State.TASK_APPEARED; + mPipTransitionState.setTransitionState(PipTransitionState.TASK_APPEARED); mLeash = leash; mPictureInPictureParams = mTaskInfo.pictureInPictureParams; setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, @@ -530,7 +517,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mOnDisplayIdChangeCallback.accept(info.displayId); } - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { if (!mWaitForFixedRotation) { onEndOfSwipePipToHomeTransition(); } else { @@ -557,6 +544,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (Transitions.ENABLE_SHELL_TRANSITIONS) { if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { mPipMenuController.attach(mLeash); + } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + mOneShotAnimationType = ANIM_TYPE_BOUNDS; } return; } @@ -568,7 +557,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, null /* updateBoundsCallback */); - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -595,7 +584,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); animateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */); - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } /** @@ -620,7 +609,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceControlTransactionFactory.getTransaction(); tx.setAlpha(mLeash, 0f); tx.apply(); - mState = State.ENTRY_SCHEDULED; + mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); applyEnterPipSyncTransaction(destinationBounds, () -> { mPipAnimationController .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f) @@ -631,11 +620,16 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, .start(); // mState is set right after the animation is kicked off to block any resize // requests such as offsetPip that may have been called prior to the transition. - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); }, null /* boundsChangeTransaction */); } private void onEndOfSwipePipToHomeTransition() { + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mSwipePipToHomeOverlay = null; + return; + } + final Rect destinationBounds = mPipBoundsState.getBounds(); final SurfaceControl swipeToHomeOverlay = mSwipePipToHomeOverlay; final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); @@ -655,7 +649,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, null /* callback */, false /* withStartDelay */); } }, tx); - mInSwipePipToHomeTransition = false; + mPipTransitionState.setInSwipePipToHomeTransition(false); mSwipePipToHomeOverlay = null; } @@ -679,7 +673,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, private void sendOnPipTransitionStarted( @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { - mState = State.ENTERING_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); } mPipTransitionController.sendOnPipTransitionStarted(direction); } @@ -688,7 +682,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, void sendOnPipTransitionFinished( @PipAnimationController.TransitionDirection int direction) { if (direction == TRANSITION_DIRECTION_TO_PIP) { - mState = State.ENTERED_PIP; + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); } mPipTransitionController.sendOnPipTransitionFinished(direction); // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent. @@ -713,7 +707,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ @Override public void onTaskVanished(ActivityManager.RunningTaskInfo info) { - if (mState == State.UNDEFINED) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { return; } final WindowContainerToken token = info.token; @@ -723,9 +717,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, return; } clearWaitForFixedRotation(); - mInSwipePipToHomeTransition = false; + mPipTransitionState.setInSwipePipToHomeTransition(false); mPictureInPictureParams = null; - mState = State.UNDEFINED; + mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); // Re-set the PIP bounds to none. mPipBoundsState.setBounds(new Rect()); mPipUiEventLoggerLogger.setTaskInfo(null); @@ -735,6 +729,9 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); } + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + mPipTransitionController.forceFinishTransition(); + } final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController.getCurrentAnimator(); if (animator != null) { @@ -750,8 +747,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); - if (mState != State.ENTERED_PIP && mState != State.EXITING_PIP) { - Log.d(TAG, "Defer onTaskInfoChange in current state: " + mState); + if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP + && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) { + Log.d(TAG, "Defer onTaskInfoChange in current state: " + + mPipTransitionState.getTransitionState()); // Defer applying PiP parameters if the task is entering PiP to avoid disturbing // the animation. mDeferredTaskInfo = info; @@ -774,8 +773,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } @Override - public boolean supportSizeCompatUI() { - // PIP doesn't support size compat. + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + mPipMenuController.onFocusTaskChanged(taskInfo); + } + + @Override + public boolean supportCompatUI() { + // PIP doesn't support compat. return false; } @@ -784,7 +788,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mNextRotation = newRotation; mWaitForFixedRotation = true; - if (mState.isInPip()) { + if (mPipTransitionState.isInPip()) { // Fade out the existing PiP to avoid jump cut during seamless rotation. fadeExistingPip(false /* show */); } @@ -795,17 +799,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, if (!mWaitForFixedRotation) { return; } - if (mState == State.TASK_APPEARED) { - if (mInSwipePipToHomeTransition) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) { + if (mPipTransitionState.getInSwipePipToHomeTransition()) { onEndOfSwipePipToHomeTransition(); } else { // Schedule a regular animation to ensure all the callbacks are still being sent. enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(), mEnterAnimationDuration); } - } else if (mState == State.ENTERED_PIP && mHasFadeOut) { + } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERED_PIP + && mHasFadeOut) { fadeExistingPip(true /* show */); - } else if (mState == State.ENTERING_PIP && mDeferredAnimEndTransaction != null) { + } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERING_PIP + && mDeferredAnimEndTransaction != null) { final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController.getCurrentAnimator(); final Rect destinationBounds = animator.getDestinationBounds(); @@ -859,13 +865,15 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, // note that this can be called when swipe-to-home or fixed-rotation is happening. // Skip this entirely if that's the case. final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation - && (mState != State.ENTERED_PIP); - if ((mInSwipePipToHomeTransition || waitForFixedRotationOnEnteringPip) && fromRotation) { + && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP); + if ((mPipTransitionState.getInSwipePipToHomeTransition() + || waitForFixedRotationOnEnteringPip) && fromRotation) { if (DEBUG) { Log.d(TAG, "Skip onMovementBoundsChanged on rotation change" - + " mInSwipePipToHomeTransition=" + mInSwipePipToHomeTransition + + " InSwipePipToHomeTransition=" + + mPipTransitionState.getInSwipePipToHomeTransition() + " mWaitForFixedRotation=" + mWaitForFixedRotation - + " mState=" + mState); + + " getTransitionState=" + mPipTransitionState.getTransitionState()); } return; } @@ -873,7 +881,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipAnimationController.getCurrentAnimator(); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { - final boolean rotatingPip = mState.isInPip() && fromRotation; + final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation; if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { // The position will be used by fade-in animation when the fixed rotation is done. mPipBoundsState.setBounds(destinationBoundsOut); @@ -1006,7 +1014,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback) { - if (!mState.isInPip()) { + if (!mPipTransitionState.isInPip()) { // TODO: tend to use shouldBlockResizeRequest here as well but need to consider // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window // container transaction callback and we want to set the mState immediately. @@ -1036,7 +1044,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, toBounds) - .round(tx, mLeash, mState.isInPip()); + .round(tx, mLeash, mPipTransitionState.isInPip()); if (mPipMenuController.isMenuVisible()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); } else { @@ -1114,7 +1122,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, public void scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback) { - if (mState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest()) { return; } @@ -1131,7 +1139,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) - .round(tx, mLeash, mState.isInPip()); + .round(tx, mLeash, mPipTransitionState.isInPip()); return tx; } @@ -1140,7 +1148,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { - if (mState.shouldBlockResizeRequest()) { + if (mPipTransitionState.shouldBlockResizeRequest()) { return; } if (mWaitForFixedRotation) { @@ -1170,6 +1178,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type) { final Rect preResizeBounds = new Rect(mPipBoundsState.getBounds()); + final boolean isPipTopLeft = isPipTopLeft(); mPipBoundsState.setBounds(destinationBounds); if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { removePipImmediately(); @@ -1215,10 +1224,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, null /* callback */, false /* withStartDelay */); }); } else { - applyFinishBoundsResize(wct, direction); + applyFinishBoundsResize(wct, direction, isPipTopLeft); } } else { - applyFinishBoundsResize(wct, direction); + applyFinishBoundsResize(wct, direction, isPipTopLeft); } finishResizeForMenu(destinationBounds); @@ -1266,8 +1275,23 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, * applying it. */ public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, - @PipAnimationController.TransitionDirection int direction) { - mTaskOrganizer.applyTransaction(wct); + @PipAnimationController.TransitionDirection int direction, boolean wasPipTopLeft) { + if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { + mSplitScreenOptional.get().enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct); + } else { + mTaskOrganizer.applyTransaction(wct); + } + } + + private boolean isPipTopLeft() { + if (!mSplitScreenOptional.isPresent()) { + return false; + } + final Rect topLeft = new Rect(); + final Rect bottomRight = new Rect(); + mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); + + return topLeft.contains(mPipBoundsState.getBounds()); } /** @@ -1297,13 +1321,17 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE ? mPipBoundsState.getBounds() : currentBounds; + final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null + && mPipAnimationController.getCurrentAnimator().isRunning(); final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, sourceHintRect, direction, startingAngle, rotationDelta); animator.setTransitionDirection(direction) - .setPipAnimationCallback(mPipAnimationCallback) .setPipTransactionHandler(mPipTransactionHandler) .setDuration(durationMs); + if (!existingAnimatorRunning) { + animator.setPipAnimationCallback(mPipAnimationCallback); + } if (isInPipDirection(direction)) { // Similar to auto-enter-pip transition, we use content overlay when there is no // source rect hint to enter PiP use bounds animation. @@ -1348,18 +1376,27 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } /** - * Sync with {@link LegacySplitScreenController} on destination bounds if PiP is going to split - * screen. + * Sync with {@link LegacySplitScreenController} or {@link SplitScreenController} on destination + * bounds if PiP is going to split screen. * * @param destinationBoundsOut contain the updated destination bounds if applicable * @return {@code true} if destinationBounds is altered for split screen */ - private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) { - if (!mSplitScreenOptional.isPresent()) { + private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit) { + if (enterSplit && mSplitScreenOptional.isPresent()) { + final Rect topLeft = new Rect(); + final Rect bottomRight = new Rect(); + mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); + final boolean isPipTopLeft = isPipTopLeft(); + destinationBoundsOut.set(isPipTopLeft ? topLeft : bottomRight); + return true; + } + + if (!mLegacySplitScreenOptional.isPresent()) { return false; } - LegacySplitScreenController legacySplitScreen = mSplitScreenOptional.get(); + LegacySplitScreenController legacySplitScreen = mLegacySplitScreenOptional.get(); if (!legacySplitScreen.isDividerVisible()) { // fail early if system is not in split screen mode return false; @@ -1384,7 +1421,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f); animator.setDuration(mCrossFadeAnimationDuration); animator.addUpdateListener(animation -> { - if (mState == State.UNDEFINED) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Could happen if onTaskVanished happens during the animation since we may have // set a start delay on this animation. Log.d(TAG, "Task vanished, skip fadeOutAndRemoveOverlay"); @@ -1410,7 +1447,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, } private void removeContentOverlay(SurfaceControl surface, Runnable callback) { - if (mState == State.UNDEFINED) { + if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { // Avoid double removal, which is fatal. return; } @@ -1432,7 +1469,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, pw.println(innerPrefix + "mToken=" + mToken + " binder=" + (mToken != null ? mToken.asBinder() : null)); pw.println(innerPrefix + "mLeash=" + mLeash); - pw.println(innerPrefix + "mState=" + mState); + pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState()); pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 4759550c35c0..b31e6e0750ce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -18,6 +18,10 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.util.RotationUtils.deltaRotation; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_PIP; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; @@ -25,11 +29,16 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; +import android.app.ActivityManager; import android.app.TaskInfo; import android.content.Context; +import android.graphics.Matrix; import android.graphics.Rect; import android.os.IBinder; +import android.util.Log; import android.view.Surface; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -49,74 +58,275 @@ import com.android.wm.shell.transition.Transitions; */ public class PipTransition extends PipTransitionController { + private static final String TAG = PipTransition.class.getSimpleName(); + + private final PipTransitionState mPipTransitionState; private final int mEnterExitAnimationDuration; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private Transitions.TransitionFinishCallback mFinishCallback; + private Rect mExitDestinationBounds = new Rect(); + private IBinder mExitTransition = null; public PipTransition(Context context, - PipBoundsState pipBoundsState, PipMenuController pipMenuController, + PipBoundsState pipBoundsState, + PipTransitionState pipTransitionState, + PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @NonNull ShellTaskOrganizer shellTaskOrganizer) { super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + mPipTransitionState = pipTransitionState; mEnterExitAnimationDuration = context.getResources() .getInteger(R.integer.config_pipResizeAnimationDuration); } @Override + public void setIsFullAnimation(boolean isFullAnimation) { + setOneShotAnimationType(isFullAnimation ? ANIM_TYPE_BOUNDS : ANIM_TYPE_ALPHA); + } + + /** + * Sets the preferred animation type for one time. + * This is typically used to set the animation type to + * {@link PipAnimationController#ANIM_TYPE_ALPHA}. + */ + private void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { + mOneShotAnimationType = animationType; + } + + @Override + public void startTransition(Rect destinationBounds, WindowContainerTransaction out) { + if (destinationBounds != null) { + mExitDestinationBounds.set(destinationBounds); + mExitTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); + } else { + mTransitions.startTransition(TRANSIT_REMOVE_PIP, out, this); + } + } + + @Override public boolean startAnimation(@android.annotation.NonNull IBinder transition, @android.annotation.NonNull TransitionInfo info, - @android.annotation.NonNull SurfaceControl.Transaction t, + @android.annotation.NonNull SurfaceControl.Transaction startTransaction, + @android.annotation.NonNull SurfaceControl.Transaction finishTransaction, @android.annotation.NonNull Transitions.TransitionFinishCallback finishCallback) { + + if (mExitTransition == transition || info.getType() == TRANSIT_EXIT_PIP) { + mExitTransition = null; + if (info.getChanges().size() == 1) { + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(null, null); + mFinishCallback = null; + throw new RuntimeException("Previous callback not called, aborting exit PIP."); + } + + final TransitionInfo.Change change = info.getChanges().get(0); + mFinishCallback = finishCallback; + startTransaction.apply(); + boolean success = startExpandAnimation(change.getTaskInfo(), change.getLeash(), + new Rect(mExitDestinationBounds)); + mExitDestinationBounds.setEmpty(); + return success; + } else { + Log.e(TAG, "Got an exit-pip transition with unexpected change-list"); + } + } + + if (info.getType() == TRANSIT_REMOVE_PIP) { + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */); + mFinishCallback = null; + throw new RuntimeException("Previous callback not called, aborting remove PIP."); + } + + startTransaction.apply(); + finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(), + mPipBoundsState.getDisplayBounds()); + finishCallback.onTransitionFinished(null, null); + return true; + } + + // We only support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps + // that enter PiP instantly on opening, mostly from CTS/Flicker tests) + if (info.getType() != TRANSIT_PIP && info.getType() != TRANSIT_OPEN) { + return false; + } + + // Search for an Enter PiP transition (along with a show wallpaper one) + TransitionInfo.Change enterPip = null; + TransitionInfo.Change wallpaper = null; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); if (change.getTaskInfo() != null && change.getTaskInfo().configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED) { - mFinishCallback = finishCallback; - return startEnterAnimation(change.getTaskInfo(), change.getLeash(), t); + enterPip = change; + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + wallpaper = change; } } - return false; + if (enterPip == null) { + return false; + } + + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */); + mFinishCallback = null; + throw new RuntimeException("Previous callback not called, aborting entering PIP."); + } + + // Show the wallpaper if there is a wallpaper change. + if (wallpaper != null) { + startTransaction.show(wallpaper.getLeash()); + startTransaction.setAlpha(wallpaper.getLeash(), 1.f); + } + + mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); + mFinishCallback = finishCallback; + return startEnterAnimation(enterPip.getTaskInfo(), enterPip.getLeash(), + startTransaction, finishTransaction, enterPip.getStartRotation(), + enterPip.getEndRotation()); } @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request) { - return null; + if (request.getType() == TRANSIT_PIP) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); + if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { + wct.setActivityWindowingMode(request.getTriggerTask().token, + WINDOWING_MODE_UNDEFINED); + final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + wct.setBounds(request.getTriggerTask().token, destinationBounds); + } + return wct; + } else { + return null; + } + } + + @Override + public void onTransitionMerged(@NonNull IBinder transition) { + if (transition != mExitTransition) { + return; + } + // This means an expand happened before enter-pip finished and we are now "merging" a + // no-op transition that happens to match our exit-pip. + boolean cancelled = false; + if (mPipAnimationController.getCurrentAnimator() != null) { + mPipAnimationController.getCurrentAnimator().cancel(); + cancelled = true; + } + // Unset exitTransition AFTER cancel so that finishResize knows we are merging. + mExitTransition = null; + if (!cancelled) return; + final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo(); + if (taskInfo != null) { + startExpandAnimation(taskInfo, mPipOrganizer.getSurfaceControl(), + new Rect(mExitDestinationBounds)); + } + mExitDestinationBounds.setEmpty(); } @Override public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, - SurfaceControl.Transaction tx) { - WindowContainerTransaction wct = new WindowContainerTransaction(); - prepareFinishResizeTransaction(taskInfo, destinationBounds, - direction, tx, wct); - mFinishCallback.onTransitionFinished(wct, null); + @Nullable SurfaceControl.Transaction tx) { + + if (isInPipDirection(direction)) { + mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); + } + // If there is an expected exit transition, then the exit will be "merged" into this + // transition so don't fire the finish-callback in that case. + if (mExitTransition == null && mFinishCallback != null) { + WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareFinishResizeTransaction(taskInfo, destinationBounds, + direction, wct); + if (tx != null) { + wct.setBoundsChangeTransaction(taskInfo.token, tx); + } + mFinishCallback.onTransitionFinished(wct, null /* callback */); + mFinishCallback = null; + } finishResizeForMenu(destinationBounds); } + @Override + public void forceFinishTransition() { + if (mFinishCallback == null) return; + mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */); + mFinishCallback = null; + } + + private boolean startExpandAnimation(final TaskInfo taskInfo, final SurfaceControl leash, + final Rect destinationBounds) { + PipAnimationController.PipTransitionAnimator animator = + mPipAnimationController.getAnimator(taskInfo, leash, mPipBoundsState.getBounds(), + mPipBoundsState.getBounds(), destinationBounds, null, + TRANSITION_DIRECTION_LEAVE_PIP, 0 /* startingAngle */, Surface.ROTATION_0); + + animator.setTransitionDirection(TRANSITION_DIRECTION_LEAVE_PIP) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start(); + + return true; + } + private boolean startEnterAnimation(final TaskInfo taskInfo, final SurfaceControl leash, - final SurfaceControl.Transaction t) { + final SurfaceControl.Transaction startTransaction, + final SurfaceControl.Transaction finishTransaction, + final int startRotation, final int endRotation) { setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams, taskInfo.topActivityInfo); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = taskInfo.configuration.windowConfiguration.getBounds(); PipAnimationController.PipTransitionAnimator animator; + finishTransaction.setPosition(leash, destinationBounds.left, destinationBounds.top); + if (taskInfo.pictureInPictureParams != null + && taskInfo.pictureInPictureParams.isAutoEnterEnabled() + && mPipTransitionState.getInSwipePipToHomeTransition()) { + mOneShotAnimationType = ANIM_TYPE_BOUNDS; + + // PiP menu is attached late in the process here to avoid any artifacts on the leash + // caused by addShellRoot when in gesture navigation mode. + mPipMenuController.attach(leash); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, new float[9]) + .setPosition(leash, destinationBounds.left, destinationBounds.top) + .setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()); + startTransaction.merge(tx); + startTransaction.apply(); + mPipBoundsState.setBounds(destinationBounds); + onFinishResize(taskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, null /* tx */); + sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); + mPipTransitionState.setInSwipePipToHomeTransition(false); + return true; + } + + int rotationDelta = deltaRotation(endRotation, startRotation); + if (rotationDelta != Surface.ROTATION_0) { + Matrix tmpTransform = new Matrix(); + tmpTransform.postRotate(rotationDelta == Surface.ROTATION_90 + ? Surface.ROTATION_270 : Surface.ROTATION_90); + startTransaction.setMatrix(leash, tmpTransform, new float[9]); + } if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( taskInfo.pictureInPictureParams, currentBounds); animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds, currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, - 0 /* startingAngle */, Surface.ROTATION_0); + 0 /* startingAngle */, rotationDelta); } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { - t.setAlpha(leash, 0f); - t.apply(); + startTransaction.setAlpha(leash, 0f); + // PiP menu is attached late in the process here to avoid any artifacts on the leash + // caused by addShellRoot when in gesture navigation mode. + mPipMenuController.attach(leash); animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds, 0f, 1f); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -124,10 +334,12 @@ public class PipTransition extends PipTransitionController { throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); } + startTransaction.apply(); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(mEnterExitAnimationDuration) .start(); + return true; } @@ -138,7 +350,6 @@ public class PipTransition extends PipTransitionController { private void prepareFinishResizeTransaction(TaskInfo taskInfo, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, - SurfaceControl.Transaction tx, WindowContainerTransaction wct) { Rect taskBounds = null; if (isInPipDirection(direction)) { @@ -158,6 +369,5 @@ public class PipTransition extends PipTransitionController { } wct.setBounds(taskInfo.token, taskBounds); - wct.setBoundsChangeTransaction(taskInfo.token, tx); } } 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 d801c918973a..376f3298a83c 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 @@ -19,7 +19,6 @@ package com.android.wm.shell.pip; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; -import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; import android.app.PictureInPictureParams; import android.app.TaskInfo; @@ -29,6 +28,7 @@ import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.transition.Transitions; @@ -46,8 +46,10 @@ public abstract class PipTransitionController implements Transitions.TransitionH protected final PipBoundsState mPipBoundsState; protected final ShellTaskOrganizer mShellTaskOrganizer; protected final PipMenuController mPipMenuController; + protected final Transitions mTransitions; private final Handler mMainHandler; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); + protected PipTaskOrganizer mPipOrganizer; protected final PipAnimationController.PipAnimationCallback mPipAnimationCallback = new PipAnimationController.PipAnimationCallback() { @@ -55,12 +57,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH public void onPipAnimationStart(TaskInfo taskInfo, PipAnimationController.PipTransitionAnimator animator) { final int direction = animator.getTransitionDirection(); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // TODO (b//169221267): Add jank listener for transactions without buffer - // updates. - //InteractionJankMonitor.getInstance().begin( - // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); - } sendOnPipTransitionStarted(direction); } @@ -74,12 +70,6 @@ public abstract class PipTransitionController implements Transitions.TransitionH } onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx); sendOnPipTransitionFinished(direction); - if (direction == TRANSITION_DIRECTION_TO_PIP) { - // TODO (b//169221267): Add jank listener for transactions without buffer - // updates. - //InteractionJankMonitor.getInstance().end( - // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP); - } } @Override @@ -98,6 +88,29 @@ public abstract class PipTransitionController implements Transitions.TransitionH SurfaceControl.Transaction tx) { } + /** + * Called to inform the transition that the animation should start with the assumption that + * PiP is not animating from its original bounds, but rather a continuation of another + * animation. For example, gesture navigation would first fade out the PiP activity, and the + * transition should be responsible to animate in (such as fade in) the PiP. + */ + public void setIsFullAnimation(boolean isFullAnimation) { + } + + /** + * Called when the Shell wants to starts a transition/animation. + */ + public void startTransition(Rect destinationBounds, WindowContainerTransaction out) { + // Default implementation does nothing. + } + + /** + * Called when the transition animation can't continue (eg. task is removed during + * animation) + */ + public void forceFinishTransition() { + } + public PipTransitionController(PipBoundsState pipBoundsState, PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @@ -107,12 +120,17 @@ public abstract class PipTransitionController implements Transitions.TransitionH mShellTaskOrganizer = shellTaskOrganizer; mPipBoundsAlgorithm = pipBoundsAlgorithm; mPipAnimationController = pipAnimationController; + mTransitions = transitions; mMainHandler = new Handler(Looper.getMainLooper()); if (Transitions.ENABLE_SHELL_TRANSITIONS) { transitions.addHandler(this); } } + void setPipOrganizer(PipTaskOrganizer pto) { + mPipOrganizer = pto; + } + /** * Registers {@link PipTransitionCallback} to receive transition callbacks. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java new file mode 100644 index 000000000000..85e56b7dd99f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionState.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2021 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.pip; + +import android.annotation.IntDef; +import android.app.PictureInPictureParams; +import android.content.ComponentName; +import android.content.pm.ActivityInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Used to keep track of PiP leash state as it appears and animates by {@link PipTaskOrganizer} and + * {@link PipTransition}. + */ +public class PipTransitionState { + + public static final int UNDEFINED = 0; + public static final int TASK_APPEARED = 1; + public static final int ENTRY_SCHEDULED = 2; + public static final int ENTERING_PIP = 3; + public static final int ENTERED_PIP = 4; + public static final int EXITING_PIP = 5; + + /** + * If set to {@code true}, no entering PiP transition would be kicked off and most likely + * it's due to the fact that Launcher is handling the transition directly when swiping + * auto PiP-able Activity to home. + * See also {@link PipTaskOrganizer#startSwipePipToHome(ComponentName, ActivityInfo, + * PictureInPictureParams)}. + */ + private boolean mInSwipePipToHomeTransition; + + // Not a complete set of states but serves what we want right now. + @IntDef(prefix = { "TRANSITION_STATE_" }, value = { + UNDEFINED, + TASK_APPEARED, + ENTRY_SCHEDULED, + ENTERING_PIP, + ENTERED_PIP, + EXITING_PIP + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransitionState {} + + private @TransitionState int mState; + + public PipTransitionState() { + mState = UNDEFINED; + } + + public void setTransitionState(@TransitionState int state) { + mState = state; + } + + public @TransitionState int getTransitionState() { + return mState; + } + + public boolean isInPip() { + return mState >= TASK_APPEARED + && mState != EXITING_PIP; + } + + public void setInSwipePipToHomeTransition(boolean inSwipePipToHomeTransition) { + mInSwipePipToHomeTransition = inSwipePipToHomeTransition; + } + + public boolean getInSwipePipToHomeTransition() { + return mInSwipePipToHomeTransition; + } + /** + * Resize request can be initiated in other component, ignore if we are no longer in PIP, + * still waiting for animation or we're exiting from it. + * + * @return {@code true} if the resize request should be blocked/ignored. + */ + public boolean shouldBlockResizeRequest() { + return mState < ENTERING_PIP + || mState == EXITING_PIP; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index a646b07c49dc..101a55d8d367 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip.phone; import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.RemoteAction; import android.content.Context; import android.content.pm.ParceledListSlice; @@ -43,10 +44,12 @@ import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMediaController.ActionListener; import com.android.wm.shell.pip.PipMenuController; +import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * Manages the PiP menu view which can show menu options or a scrim. @@ -60,8 +63,7 @@ public class PhonePipMenuController implements PipMenuController { private static final boolean DEBUG = false; public static final int MENU_STATE_NONE = 0; - public static final int MENU_STATE_CLOSE = 1; - public static final int MENU_STATE_FULL = 2; + public static final int MENU_STATE_FULL = 1; /** * A listener interface to receive notification on changes in PIP. @@ -96,6 +98,11 @@ public class PhonePipMenuController implements PipMenuController { * Called when the PIP requested to show the menu. */ void onPipShowMenu(); + + /** + * Called when the PIP requested to enter Split. + */ + void onEnterSplit(); } private final Matrix mMoveTransform = new Matrix(); @@ -110,6 +117,7 @@ public class PhonePipMenuController implements PipMenuController { private final ArrayList<Listener> mListeners = new ArrayList<>(); private final SystemWindows mSystemWindows; + private final Optional<SplitScreenController> mSplitScreenController; private ParceledListSlice<RemoteAction> mAppActions; private ParceledListSlice<RemoteAction> mMediaActions; private SyncRtSurfaceTransactionApplier mApplier; @@ -141,6 +149,7 @@ public class PhonePipMenuController implements PipMenuController { public PhonePipMenuController(Context context, PipBoundsState pipBoundsState, PipMediaController mediaController, SystemWindows systemWindows, + Optional<SplitScreenController> splitScreenOptional, ShellExecutor mainExecutor, Handler mainHandler) { mContext = context; mPipBoundsState = pipBoundsState; @@ -148,6 +157,7 @@ public class PhonePipMenuController implements PipMenuController { mSystemWindows = systemWindows; mMainExecutor = mainExecutor; mMainHandler = mainHandler; + mSplitScreenController = splitScreenOptional; } public boolean isMenuVisible() { @@ -176,10 +186,12 @@ public class PhonePipMenuController implements PipMenuController { if (mPipMenuView != null) { detachPipMenuView(); } - mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler); + mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler, + mSplitScreenController); mSystemWindows.addView(mPipMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), 0, SHELL_ROOT_LAYER_PIP); + setShellRootAccessibilityWindow(); } private void detachPipMenuView() { @@ -205,6 +217,13 @@ public class PhonePipMenuController implements PipMenuController { updateMenuLayout(destinationBounds); } + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mPipMenuView != null) { + mPipMenuView.onFocusTaskChanged(taskInfo); + } + } + /** * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some * reason (ie. the window isn't ready yet, thus {@link android.view.ViewRootImpl} is @@ -459,6 +478,10 @@ public class PhonePipMenuController implements PipMenuController { mListeners.forEach(Listener::onPipDismiss); } + void onEnterSplit() { + mListeners.forEach(Listener::onEnterSplit); + } + /** * @return the best set of actions to show in the PiP menu. */ @@ -524,6 +547,10 @@ public class PhonePipMenuController implements PipMenuController { mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState)); } mMenuState = menuState; + setShellRootAccessibilityWindow(); + } + + private void setShellRootAccessibilityWindow() { switch (mMenuState) { case MENU_STATE_NONE: mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java index 47a8c67a22e6..69ae45d12795 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipAccessibilityInteractionConnection.java @@ -151,7 +151,7 @@ public class PipAccessibilityInteractionConnection { result = true; break; case AccessibilityNodeInfo.ACTION_EXPAND: - mMotionHelper.expandLeavePip(); + mMotionHelper.expandLeavePip(false /* skipAnimation */); result = true; break; default: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 8967457802a7..a41fd8429e35 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -21,7 +21,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; import static android.view.WindowManager.INPUT_CONSUMER_PIP; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; +import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; import android.app.ActivityManager; @@ -34,7 +43,6 @@ import android.content.pm.ActivityInfo; import android.content.pm.ParceledListSlice; import android.content.res.Configuration; import android.graphics.Rect; -import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; @@ -52,6 +60,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayChangeController; @@ -59,6 +68,7 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.onehanded.OneHandedController; @@ -67,6 +77,7 @@ import com.android.wm.shell.pip.IPip; import com.android.wm.shell.pip.IPipAnimationListener; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; +import com.android.wm.shell.pip.PipAnimationController; import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; @@ -74,6 +85,7 @@ import com.android.wm.shell.pip.PipSnapAlgorithm; import com.android.wm.shell.pip.PipTaskOrganizer; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.Optional; @@ -104,13 +116,28 @@ public class PipController implements PipTransitionController.PipTransitionCallb private final Rect mTmpInsetBounds = new Rect(); private boolean mIsInFixedRotation; - private IPipAnimationListener mPinnedStackAnimationRecentsCallback; + private PipAnimationListener mPinnedStackAnimationRecentsCallback; protected PhonePipMenuController mMenuController; protected PipTaskOrganizer mPipTaskOrganizer; protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener = new PipControllerPinnedTaskListener(); + private interface PipAnimationListener { + /** + * Notifies the listener that the Pip animation is started. + */ + void onPipAnimationStarted(); + + /** + * Notifies the listener about PiP round corner radius changes. + * Listener can expect an immediate callback the first time they attach. + * + * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. + */ + void onPipCornerRadiusChanged(int cornerRadius); + } + /** * Handler for display rotation changes. */ @@ -435,6 +462,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void onOverlayChanged() { + mTouchHandler.onOverlayChanged(); onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()), false /* saveRestoreSnapFraction */); } @@ -444,11 +472,19 @@ public class PipController implements PipTransitionController.PipTransitionCallb return; } Runnable updateDisplayLayout = () -> { + final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS + && mPipBoundsState.getDisplayLayout().rotation() != layout.rotation(); mPipBoundsState.setDisplayLayout(layout); + final WindowContainerTransaction wct = + fromRotation ? new WindowContainerTransaction() : null; updateMovementBounds(null /* toBounds */, - false /* fromRotation */, false /* fromImeAdjustment */, + fromRotation, false /* fromImeAdjustment */, false /* fromShelfAdjustment */, - null /* windowContainerTransaction */); + wct /* windowContainerTransaction */); + if (wct != null) { + mPipTaskOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_SAME, + false /* wasPipTopLeft */); + } }; if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { @@ -527,9 +563,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb private void setPinnedStackAnimationType(int animationType) { mPipTaskOrganizer.setOneShotAnimationType(animationType); + mPipTransitionController.setIsFullAnimation( + animationType == PipAnimationController.ANIM_TYPE_BOUNDS); } - private void setPinnedStackAnimationListener(IPipAnimationListener callback) { + private void setPinnedStackAnimationListener(PipAnimationListener callback) { mPinnedStackAnimationRecentsCallback = callback; onPipCornerRadiusChanged(); } @@ -538,11 +576,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb if (mPinnedStackAnimationRecentsCallback != null) { final int cornerRadius = mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius); - try { - mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius); - } catch (RemoteException e) { - Log.e(TAG, "Failed to call onPipCornerRadiusChanged", e); - } + mPinnedStackAnimationRecentsCallback.onPipCornerRadiusChanged(cornerRadius); } } @@ -563,8 +597,37 @@ public class PipController implements PipTransitionController.PipTransitionCallb mPipTaskOrganizer.stopSwipePipToHome(componentName, destinationBounds, overlay); } + private String getTransitionTag(int direction) { + switch (direction) { + case TRANSITION_DIRECTION_TO_PIP: + return "TRANSITION_TO_PIP"; + case TRANSITION_DIRECTION_LEAVE_PIP: + return "TRANSITION_LEAVE_PIP"; + case TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN: + return "TRANSITION_LEAVE_PIP_TO_SPLIT_SCREEN"; + case TRANSITION_DIRECTION_REMOVE_STACK: + return "TRANSITION_REMOVE_STACK"; + case TRANSITION_DIRECTION_SNAP_AFTER_RESIZE: + return "TRANSITION_SNAP_AFTER_RESIZE"; + case TRANSITION_DIRECTION_USER_RESIZE: + return "TRANSITION_USER_RESIZE"; + case TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND: + return "TRANSITION_EXPAND_OR_UNEXPAND"; + default: + return "TRANSITION_LEAVE_UNKNOWN"; + } + } + @Override public void onPipTransitionStarted(int direction, Rect pipBounds) { + // Begin InteractionJankMonitor with PIP transition CUJs + final InteractionJankMonitor.Configuration.Builder builder = + InteractionJankMonitor.Configuration.Builder.withSurface( + CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl()) + .setTag(getTransitionTag(direction)) + .setTimeout(2000); + InteractionJankMonitor.getInstance().begin(builder); + if (isOutPipDirection(direction)) { // Exiting PIP, save the reentry state to restore to when re-entering. saveReentryState(pipBounds); @@ -572,11 +635,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb // Disable touches while the animation is running mTouchHandler.setTouchEnabled(false); if (mPinnedStackAnimationRecentsCallback != null) { - try { - mPinnedStackAnimationRecentsCallback.onPipAnimationStarted(); - } catch (RemoteException e) { - Log.e(TAG, "Failed to call onPinnedStackAnimationStarted()", e); - } + mPinnedStackAnimationRecentsCallback.onPipAnimationStarted(); } } @@ -603,6 +662,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb } private void onPipTransitionFinishedOrCanceled(int direction) { + // End InteractionJankMonitor with PIP transition by CUJs + InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION); + // Re-enable touches after the animation completes mTouchHandler.setTouchEnabled(true); mTouchHandler.onPinnedStackAnimationEnded(direction); @@ -781,9 +843,16 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setPipExclusionBoundsChangeListener(Consumer<Rect> listener) { + public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { + mMainExecutor.execute(() -> { + mPipBoundsState.addPipExclusionBoundsChangeCallback(listener); + }); + } + + @Override + public void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { mMainExecutor.execute(() -> { - mPipBoundsState.setPipExclusionBoundsChangeCallback(listener); + mPipBoundsState.removePipExclusionBoundsChangeCallback(listener); }); } @@ -812,22 +881,25 @@ public class PipController implements PipTransitionController.PipTransitionCallb @BinderThread private static class IPipImpl extends IPip.Stub { private PipController mController; - private IPipAnimationListener mListener; - private final IBinder.DeathRecipient mListenerDeathRecipient = - new IBinder.DeathRecipient() { - @Override - @BinderThread - public void binderDied() { - final PipController controller = mController; - controller.getRemoteCallExecutor().execute(() -> { - mListener = null; - controller.setPinnedStackAnimationListener(null); - }); - } - }; + private final SingleInstanceRemoteListener<PipController, + IPipAnimationListener> mListener; + private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() { + @Override + public void onPipAnimationStarted() { + mListener.call(l -> l.onPipAnimationStarted()); + } + + @Override + public void onPipCornerRadiusChanged(int cornerRadius) { + mListener.call(l -> l.onPipCornerRadiusChanged(cornerRadius)); + } + }; IPipImpl(PipController controller) { mController = controller; + mListener = new SingleInstanceRemoteListener<>(mController, + c -> c.setPinnedStackAnimationListener(mPipAnimationListener), + c -> c.setPinnedStackAnimationListener(null)); } /** @@ -871,23 +943,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb public void setPinnedStackAnimationListener(IPipAnimationListener listener) { executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener", (controller) -> { - if (mListener != null) { - // Reset the old death recipient - mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } if (listener != null) { - // Register the death recipient for the new listener to clear the listener - try { - listener.asBinder().linkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to link to death"); - return; - } + mListener.register(listener); + } else { + mListener.unregister(); } - mListener = listener; - controller.setPinnedStackAnimationListener(listener); }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java index 1da9577fe49a..915c5939c34b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDismissTargetHandler.java @@ -20,6 +20,7 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M import android.content.Context; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; @@ -30,6 +31,7 @@ import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.WindowInsets; import android.view.WindowManager; import android.widget.FrameLayout; @@ -93,6 +95,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen private int mTargetSize; private int mDismissAreaHeight; private float mMagneticFieldRadiusPercent = 1f; + private WindowInsets mWindowInsets; private SurfaceControl mTaskLeash; private boolean mHasDismissTargetSurface; @@ -117,14 +120,27 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge); mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + if (mTargetViewContainer != null) { + // init can be called multiple times, remove the old one from view hierarchy first. + cleanUpDismissTarget(); + } + mTargetView = new DismissCircleView(mContext); mTargetViewContainer = new FrameLayout(mContext); mTargetViewContainer.setBackgroundDrawable( mContext.getDrawable(R.drawable.floating_dismiss_gradient_transition)); mTargetViewContainer.setClipChildren(false); mTargetViewContainer.addView(mTargetView); + mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> { + if (!windowInsets.equals(mWindowInsets)) { + mWindowInsets = windowInsets; + updateMagneticTargetSize(); + } + return windowInsets; + }); mMagnetizedPip = mMotionHelper.getMagnetizedPip(); + mMagnetizedPip.clearAllTargets(); mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0); updateMagneticTargetSize(); @@ -158,14 +174,16 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mMainExecutor.executeDelayed(() -> { - mMotionHelper.notifyDismissalPending(); - mMotionHelper.animateDismiss(); - hideDismissTargetMaybe(); - - mPipUiEventLogger.log( - PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); - }, 0); + if (mEnableDismissDragToEdge) { + mMainExecutor.executeDelayed(() -> { + mMotionHelper.notifyDismissalPending(); + mMotionHelper.animateDismiss(); + hideDismissTargetMaybe(); + + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); + }, 0); + } } }); @@ -199,10 +217,13 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen final Resources res = mContext.getResources(); mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size); mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height); + final WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets(); + final Insets navInset = insets.getInsetsIgnoringVisibility( + WindowInsets.Type.navigationBars()); final FrameLayout.LayoutParams newParams = new FrameLayout.LayoutParams(mTargetSize, mTargetSize); newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; - newParams.bottomMargin = mContext.getResources().getDimensionPixelSize( + newParams.bottomMargin = navInset.bottom + mContext.getResources().getDimensionPixelSize( R.dimen.floating_dismiss_bottom_margin); mTargetView.setLayoutParams(newParams); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java index 3eeba6eb5366..06446573840c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuIconsAlgorithm.java @@ -18,8 +18,6 @@ package com.android.wm.shell.pip.phone; import android.content.Context; import android.graphics.Rect; -import android.util.Log; -import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -34,6 +32,7 @@ public class PipMenuIconsAlgorithm { protected ViewGroup mViewRoot; protected ViewGroup mTopEndContainer; protected View mDragHandle; + protected View mEnterSplitButton; protected View mSettingsButton; protected View mDismissButton; @@ -44,14 +43,13 @@ public class PipMenuIconsAlgorithm { * Bind the necessary views. */ public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle, - View settingsButton, View dismissButton) { + View enterSplitButton, View settingsButton, View dismissButton) { mViewRoot = viewRoot; mTopEndContainer = topEndContainer; mDragHandle = dragHandle; + mEnterSplitButton = enterSplitButton; mSettingsButton = settingsButton; mDismissButton = dismissButton; - - bindInitialViewState(); } /** @@ -72,22 +70,4 @@ public class PipMenuIconsAlgorithm { v.setLayoutParams(params); } } - - /** Calculate the initial state of the menu icons. Called when the menu is first created. */ - private void bindInitialViewState() { - if (mViewRoot == null || mTopEndContainer == null || mDragHandle == null - || mSettingsButton == null || mDismissButton == null) { - Log.e(TAG, "One of the required views is null."); - return; - } - // The menu view layout starts out with the settings button aligned at the top|end of the - // view group next to the dismiss button. On phones, the settings button should be aligned - // to the top|start of the view, so move it to parent view group to then align it to the - // top|start of the menu. - mTopEndContainer.removeView(mSettingsButton); - mViewRoot.addView(mSettingsButton); - - setLayoutGravity(mDragHandle, Gravity.START | Gravity.TOP); - setLayoutGravity(mSettingsButton, Gravity.START | Gravity.TOP); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java index 67b1e6dd4cc7..e1475efcdb57 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip.phone; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; @@ -23,7 +24,6 @@ import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTR import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; -import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; @@ -33,8 +33,10 @@ import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.IntDef; +import android.app.ActivityManager; import android.app.PendingIntent.CanceledException; import android.app.RemoteAction; +import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -62,11 +64,13 @@ import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.splitscreen.SplitScreenController; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** * Translucent window that gets started on top of a task in PIP to allow the user to control it. @@ -100,12 +104,11 @@ public class PipMenuView extends FrameLayout { private static final float MENU_BACKGROUND_ALPHA = 0.3f; private static final float DISABLED_ACTION_ALPHA = 0.54f; - private static final boolean ENABLE_RESIZE_HANDLE = false; - private int mMenuState; private boolean mAllowMenuTimeout = true; private boolean mAllowTouches = true; private int mDismissFadeOutDurationMs; + private boolean mFocusedTaskAllowSplitScreen; private final List<RemoteAction> mActions = new ArrayList<>(); @@ -117,6 +120,7 @@ public class PipMenuView extends FrameLayout { private AnimatorSet mMenuContainerAnimator; private PhonePipMenuController mController; + private Optional<SplitScreenController> mSplitScreenControllerOptional; private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @@ -140,17 +144,19 @@ public class PipMenuView extends FrameLayout { protected View mViewRoot; protected View mSettingsButton; protected View mDismissButton; - protected View mResizeHandle; + protected View mEnterSplitButton; protected View mTopEndContainer; protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; public PipMenuView(Context context, PhonePipMenuController controller, - ShellExecutor mainExecutor, Handler mainHandler) { + ShellExecutor mainExecutor, Handler mainHandler, + Optional<SplitScreenController> splitScreenController) { super(context, null, 0); mContext = context; mController = controller; mMainExecutor = mainExecutor; mMainHandler = mainHandler; + mSplitScreenControllerOptional = splitScreenController; mAccessibilityManager = context.getSystemService(AccessibilityManager.class); inflate(context, R.layout.pip_menu, this); @@ -178,14 +184,23 @@ public class PipMenuView extends FrameLayout { } }); - mResizeHandle = findViewById(R.id.resize_handle); - mResizeHandle.setAlpha(0); + mEnterSplitButton = findViewById(R.id.enter_split); + mEnterSplitButton.setAlpha(0); + mEnterSplitButton.setOnClickListener(v -> { + if (mEnterSplitButton.getAlpha() != 0) { + enterSplit(); + } + }); + + findViewById(R.id.resize_handle).setAlpha(0); + mActionsGroup = findViewById(R.id.actions_group); mBetweenActionPaddingLand = getResources().getDimensionPixelSize( R.dimen.pip_between_action_padding_land); mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, - mResizeHandle, mSettingsButton, mDismissButton); + findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton, + mDismissButton); mDismissFadeOutDurationMs = context.getResources() .getInteger(R.integer.config_pipExitAnimationDuration); @@ -203,7 +218,7 @@ public class PipMenuView extends FrameLayout { @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (action == ACTION_CLICK && mMenuState == MENU_STATE_CLOSE) { + if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) { mController.showMenu(); } return super.performAccessibilityAction(host, action, args); @@ -247,10 +262,21 @@ public class PipMenuView extends FrameLayout { return super.dispatchGenericMotionEvent(event); } + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + final boolean isSplitScreen = mSplitScreenControllerOptional.isPresent() + && mSplitScreenControllerOptional.get().isTaskInSplitScreen(taskInfo.taskId); + mFocusedTaskAllowSplitScreen = isSplitScreen + || (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN + && taskInfo.supportsSplitScreenMultiWindow + && taskInfo.topActivityType != WindowConfiguration.ACTIVITY_TYPE_HOME); + } + void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { mAllowMenuTimeout = allowMenuTimeout; mDidLastShowMenuResize = resizeMenuOnShow; + final boolean enableEnterSplit = + mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton); if (mMenuState != menuState) { // Disallow touches if the menu needs to resize while showing, and we are transitioning // to/from a full menu state. @@ -269,15 +295,14 @@ public class PipMenuView extends FrameLayout { mSettingsButton.getAlpha(), 1f); ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, mDismissButton.getAlpha(), 1f); - ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, - mResizeHandle.getAlpha(), - ENABLE_RESIZE_HANDLE && menuState == MENU_STATE_CLOSE && showResizeHandle - ? 1f : 0f); + ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, + mEnterSplitButton.getAlpha(), + enableEnterSplit && mFocusedTaskAllowSplitScreen ? 1f : 0f); if (menuState == MENU_STATE_FULL) { mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, - resizeAnim); + enterSplitAnim); } else { - mMenuContainerAnimator.playTogether(dismissAnim, resizeAnim); + mMenuContainerAnimator.playTogether(enterSplitAnim); } mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS); @@ -330,7 +355,7 @@ public class PipMenuView extends FrameLayout { mMenuContainer.setAlpha(0f); mSettingsButton.setAlpha(0f); mDismissButton.setAlpha(0f); - mResizeHandle.setAlpha(0f); + mEnterSplitButton.setAlpha(0f); } void pokeMenu() { @@ -370,9 +395,10 @@ public class PipMenuView extends FrameLayout { mSettingsButton.getAlpha(), 0f); ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, mDismissButton.getAlpha(), 0f); - ObjectAnimator resizeAnim = ObjectAnimator.ofFloat(mResizeHandle, View.ALPHA, - mResizeHandle.getAlpha(), 0f); - mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, resizeAnim); + ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, + mEnterSplitButton.getAlpha(), 0f); + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, + enterSplitAnim); mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType)); mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { @@ -429,7 +455,7 @@ public class PipMenuView extends FrameLayout { FrameLayout.LayoutParams expandedLp = (FrameLayout.LayoutParams) expandContainer.getLayoutParams(); - if (mActions.isEmpty() || menuState == MENU_STATE_CLOSE || menuState == MENU_STATE_NONE) { + if (mActions.isEmpty() || menuState == MENU_STATE_NONE) { actionsContainer.setVisibility(View.INVISIBLE); // Update the expand container margin to adjust the center of the expand button to @@ -524,6 +550,14 @@ public class PipMenuView extends FrameLayout { } } + private void enterSplit() { + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); + } + + private void showSettings() { final Pair<ComponentName, Integer> topPipActivityInfo = PipUtils.getTopPipActivity(mContext); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java index c42750d62dd4..96fd59f0c911 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java @@ -337,22 +337,29 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, * Resizes the pinned stack back to unknown windowing mode, which could be freeform or * * fullscreen depending on the display area's windowing mode. */ - void expandLeavePip() { - expandLeavePip(false /* skipAnimation */); + void expandLeavePip(boolean skipAnimation) { + expandLeavePip(skipAnimation, false /* enterSplit */); + } + + /** + * Resizes the pinned task to split-screen mode. + */ + void expandIntoSplit() { + expandLeavePip(false, true /* enterSplit */); } /** * Resizes the pinned stack back to unknown windowing mode, which could be freeform or * fullscreen depending on the display area's windowing mode. */ - void expandLeavePip(boolean skipAnimation) { + private void expandLeavePip(boolean skipAnimation, boolean enterSplit) { if (DEBUG) { Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation + " callers=\n" + Debug.getCallers(5, " ")); } cancelPhysicsAnimation(); mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); - mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION); + mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION, enterSplit); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 7867f933de4f..3ace5f405d36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -22,7 +22,6 @@ import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTI import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT; import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT; -import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_CLOSE; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE; @@ -81,7 +80,6 @@ public class PipTouchHandler { private final PhonePipMenuController mMenuController; private final AccessibilityManager mAccessibilityManager; - private boolean mShowPipMenuOnAnimationEnd = false; /** * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the @@ -141,7 +139,12 @@ public class PipTouchHandler { @Override public void onPipExpand() { - mMotionHelper.expandLeavePip(); + mMotionHelper.expandLeavePip(false /* skipAnimation */); + } + + @Override + public void onEnterSplit() { + mMotionHelper.expandIntoSplit(); } @Override @@ -256,6 +259,11 @@ public class PipTouchHandler { mPipDismissTargetHandler.updateMagneticTargetSize(); } + public void onOverlayChanged() { + // onOverlayChanged is triggered upon theme change, update the dismiss target accordingly. + mPipDismissTargetHandler.init(); + } + private boolean shouldShowResizeHandle() { return false; } @@ -280,7 +288,6 @@ public class PipTouchHandler { public void onActivityPinned() { mPipDismissTargetHandler.createOrUpdateDismissTarget(); - mShowPipMenuOnAnimationEnd = true; mPipResizeGestureHandler.onActivityPinned(); mFloatingContentCoordinator.onContentAdded(mMotionHelper); } @@ -304,13 +311,6 @@ public class PipTouchHandler { // Set the initial bounds as the user resize bounds. mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds()); } - - if (mShowPipMenuOnAnimationEnd) { - mMenuController.showMenu(MENU_STATE_CLOSE, mPipBoundsState.getBounds(), - true /* allowMenuTimeout */, false /* willResizeMenu */, - shouldShowResizeHandle()); - mShowPipMenuOnAnimationEnd = false; - } } public void onConfigurationChanged() { @@ -909,7 +909,7 @@ public class PipTouchHandler { // Expand to fullscreen if this is a double tap // the PiP should be frozen until the transition ends setTouchEnabled(false); - mMotionHelper.expandLeavePip(); + mMotionHelper.expandLeavePip(false /* skipAnimation */); } } else if (mMenuState != MENU_STATE_FULL) { if (mPipBoundsState.isStashed()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index a2e9b64046fd..00083d986dbe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -219,7 +219,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void movePipToFullscreen() { if (DEBUG) Log.d(TAG, "movePipToFullscreen(), state=" + stateToName(mState)); - mPipTaskOrganizer.exitPip(mResizeAnimationDuration); + mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */); onPipDisappeared(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index b7caf72641a3..551476dc9d54 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -58,7 +58,8 @@ public class TvPipTransition extends PipTransitionController { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @android.annotation.NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl new file mode 100644 index 000000000000..6e78fcba4a00 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 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 com.android.wm.shell.recents.IRecentTasksListener; +import com.android.wm.shell.util.GroupedRecentTaskInfo; + +/** + * Interface that is exposed to remote callers to fetch recent tasks. + */ +interface IRecentTasks { + + /** + * Registers a recent tasks listener. + */ + oneway void registerRecentTasksListener(in IRecentTasksListener listener) = 1; + + /** + * Unregisters a recent tasks listener. + */ + oneway void unregisterRecentTasksListener(in IRecentTasksListener listener) = 2; + + /** + * Gets the set of recent tasks. + */ + GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) = 3; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl new file mode 100644 index 000000000000..8efa42830d80 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 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 distshellributed 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; + +/** + * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. + */ +oneway interface IRecentTasksListener { + + /** + * Called when the set of recent tasks change. + */ + void onRecentTasksChanged(); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java new file mode 100644 index 000000000000..a5748f69388f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 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 com.android.wm.shell.common.annotations.ExternalThread; + +/** + * Interface for interacting with the recent tasks. + */ +@ExternalThread +public interface RecentTasks { + /** + * Returns a binder that can be passed to an external process to fetch recent tasks. + */ + default IRecentTasks createExternalInterface() { + return null; + } +} 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 new file mode 100644 index 000000000000..338c944f7eec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2021 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 static android.app.ActivityTaskManager.INVALID_TASK_ID; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.TaskInfo; +import android.content.Context; +import android.os.RemoteException; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseIntArray; + +import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; +import com.android.wm.shell.common.TaskStackListenerCallback; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.util.GroupedRecentTaskInfo; +import com.android.wm.shell.util.StagedSplitBounds; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages the recent task list from the system, caching it as necessary. + */ +public class RecentTasksController implements TaskStackListenerCallback, + RemoteCallable<RecentTasksController> { + private static final String TAG = RecentTasksController.class.getSimpleName(); + + private final Context mContext; + private final ShellExecutor mMainExecutor; + private final TaskStackListenerImpl mTaskStackListener; + private final RecentTasks mImpl = new RecentTasksImpl(); + + private final ArrayList<Runnable> mCallbacks = new ArrayList<>(); + // Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a + // pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1) + private final SparseIntArray mSplitTasks = new SparseIntArray(); + /** + * Maps taskId to {@link StagedSplitBounds} for both taskIDs. + * Meaning there will be two taskId integers mapping to the same object. + * If there's any ordering to the pairing than we can probably just get away with only one + * taskID mapping to it, leaving both for consistency with {@link #mSplitTasks} for now. + */ + private final Map<Integer, StagedSplitBounds> mTaskSplitBoundsMap = new HashMap<>(); + + /** + * Creates {@link RecentTasksController}, returns {@code null} if the feature is not + * supported. + */ + @Nullable + public static RecentTasksController create( + Context context, + TaskStackListenerImpl taskStackListener, + @ShellMainThread ShellExecutor mainExecutor + ) { + if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) { + return null; + } + return new RecentTasksController(context, taskStackListener, mainExecutor); + } + + RecentTasksController(Context context, TaskStackListenerImpl taskStackListener, + ShellExecutor mainExecutor) { + mContext = context; + mTaskStackListener = taskStackListener; + mMainExecutor = mainExecutor; + } + + public RecentTasks asRecentTasks() { + return mImpl; + } + + public void init() { + mTaskStackListener.addListener(this); + } + + /** + * Adds a split pair. This call does not validate the taskIds, only that they are not the same. + */ + public void addSplitPair(int taskId1, int taskId2, StagedSplitBounds splitBounds) { + if (taskId1 == taskId2) { + return; + } + if (mSplitTasks.get(taskId1, INVALID_TASK_ID) == taskId2 + && mTaskSplitBoundsMap.get(taskId1).equals(splitBounds)) { + // If the two tasks are already paired and the bounds are the same, then skip updating + return; + } + // Remove any previous pairs + removeSplitPair(taskId1); + removeSplitPair(taskId2); + mTaskSplitBoundsMap.remove(taskId1); + mTaskSplitBoundsMap.remove(taskId2); + + mSplitTasks.put(taskId1, taskId2); + mSplitTasks.put(taskId2, taskId1); + mTaskSplitBoundsMap.put(taskId1, splitBounds); + mTaskSplitBoundsMap.put(taskId2, splitBounds); + notifyRecentTasksChanged(); + } + + /** + * Removes a split pair. + */ + public void removeSplitPair(int taskId) { + int pairedTaskId = mSplitTasks.get(taskId, INVALID_TASK_ID); + if (pairedTaskId != INVALID_TASK_ID) { + mSplitTasks.delete(taskId); + mSplitTasks.delete(pairedTaskId); + mTaskSplitBoundsMap.remove(taskId); + mTaskSplitBoundsMap.remove(pairedTaskId); + notifyRecentTasksChanged(); + } + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + @Override + public void onTaskStackChanged() { + notifyRecentTasksChanged(); + } + + @Override + public void onRecentTaskListUpdated() { + // In some cases immediately after booting, the tasks in the system recent task list may be + // loaded, but not in the active task hierarchy in the system. These tasks are displayed in + // overview, but removing them don't result in a onTaskStackChanged() nor a onTaskRemoved() + // callback (those are for changes to the active tasks), but the task list is still updated, + // so we should also invalidate the change id to ensure we load a new list instead of + // reusing a stale list. + notifyRecentTasksChanged(); + } + + public void onTaskRemoved(TaskInfo taskInfo) { + // Remove any split pairs associated with this task + removeSplitPair(taskInfo.taskId); + notifyRecentTasksChanged(); + } + + public void onTaskWindowingModeChanged(TaskInfo taskInfo) { + notifyRecentTasksChanged(); + } + + @VisibleForTesting + void notifyRecentTasksChanged() { + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).run(); + } + } + + private void registerRecentTasksListener(Runnable listener) { + if (!mCallbacks.contains(listener)) { + mCallbacks.add(listener); + } + } + + private void unregisterRecentTasksListener(Runnable listener) { + mCallbacks.remove(listener); + } + + @VisibleForTesting + List<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) { + return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId); + } + + @VisibleForTesting + ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) { + // Note: the returned task list is from the most-recent to least-recent order + final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags, + userId); + + // Make a mapping of task id -> task info + final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>(); + for (int i = 0; i < rawList.size(); i++) { + final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i); + rawMapping.put(taskInfo.taskId, taskInfo); + } + + // Pull out the pairs as we iterate back in the list + ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>(); + for (int i = 0; i < rawList.size(); i++) { + final ActivityManager.RecentTaskInfo taskInfo = rawList.get(i); + if (!rawMapping.contains(taskInfo.taskId)) { + // If it's not in the mapping, then it was already paired with another task + continue; + } + + final int pairedTaskId = mSplitTasks.get(taskInfo.taskId); + if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) { + final ActivityManager.RecentTaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId); + rawMapping.remove(pairedTaskId); + recentTasks.add(new GroupedRecentTaskInfo(taskInfo, pairedTaskInfo, + mTaskSplitBoundsMap.get(pairedTaskId))); + } else { + recentTasks.add(new GroupedRecentTaskInfo(taskInfo)); + } + } + return recentTasks; + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + ArrayList<GroupedRecentTaskInfo> recentTasks = getRecentTasks(Integer.MAX_VALUE, + ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser()); + for (int i = 0; i < recentTasks.size(); i++) { + pw.println(innerPrefix + recentTasks.get(i)); + } + } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + @ExternalThread + private class RecentTasksImpl implements RecentTasks { + private IRecentTasksImpl mIRecentTasks; + + @Override + public IRecentTasks createExternalInterface() { + if (mIRecentTasks != null) { + mIRecentTasks.invalidate(); + } + mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this); + return mIRecentTasks; + } + } + + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class IRecentTasksImpl extends IRecentTasks.Stub { + private RecentTasksController mController; + private final SingleInstanceRemoteListener<RecentTasksController, + IRecentTasksListener> mListener; + private final Runnable mRecentTasksListener = + new Runnable() { + @Override + public void run() { + mListener.call(l -> l.onRecentTasksChanged()); + } + }; + + public IRecentTasksImpl(RecentTasksController controller) { + mController = controller; + mListener = new SingleInstanceRemoteListener<>(controller, + c -> c.registerRecentTasksListener(mRecentTasksListener), + c -> c.unregisterRecentTasksListener(mRecentTasksListener)); + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + void invalidate() { + mController = null; + } + + @Override + public void registerRecentTasksListener(IRecentTasksListener listener) + throws RemoteException { + executeRemoteCallWithTaskPermission(mController, "registerRecentTasksListener", + (controller) -> mListener.register(listener)); + } + + @Override + public void unregisterRecentTasksListener(IRecentTasksListener listener) + throws RemoteException { + executeRemoteCallWithTaskPermission(mController, "unregisterRecentTasksListener", + (controller) -> mListener.unregister()); + } + + @Override + public GroupedRecentTaskInfo[] getRecentTasks(int maxNum, int flags, int userId) + throws RemoteException { + if (mController == null) { + // The controller is already invalidated -- just return an empty task list for now + return new GroupedRecentTaskInfo[0]; + } + + final GroupedRecentTaskInfo[][] out = new GroupedRecentTaskInfo[][]{null}; + executeRemoteCallWithTaskPermission(mController, "getRecentTasks", + (controller) -> out[0] = controller.getRecentTasks(maxNum, flags, userId) + .toArray(new GroupedRecentTaskInfo[0]), + true /* blocking */); + return out[0]; + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java deleted file mode 100644 index 78af9df30e6a..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopup.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.drawable.RippleDrawable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.Button; -import android.widget.FrameLayout; - -import androidx.annotation.Nullable; - -import com.android.wm.shell.R; - -/** Popup to show the hint about the {@link SizeCompatRestartButton}. */ -public class SizeCompatHintPopup extends FrameLayout implements View.OnClickListener { - - private SizeCompatUILayout mLayout; - - public SizeCompatHintPopup(Context context) { - super(context); - } - - public SizeCompatHintPopup(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public SizeCompatHintPopup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public SizeCompatHintPopup(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - void inject(SizeCompatUILayout layout) { - mLayout = layout; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - final Button gotItButton = findViewById(R.id.got_it); - gotItButton.setBackground(new RippleDrawable(ColorStateList.valueOf(Color.LTGRAY), - null /* content */, null /* mask */)); - gotItButton.setOnClickListener(this); - } - - @Override - public void onClick(View v) { - mLayout.dismissHint(); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java deleted file mode 100644 index 08a840297df1..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButton.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Color; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.RippleDrawable; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageButton; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.wm.shell.R; - -/** Button to restart the size compat activity. */ -public class SizeCompatRestartButton extends FrameLayout implements View.OnClickListener, - View.OnLongClickListener { - - private SizeCompatUILayout mLayout; - - public SizeCompatRestartButton(@NonNull Context context) { - super(context); - } - - public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public SizeCompatRestartButton(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - void inject(SizeCompatUILayout layout) { - mLayout = layout; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - final ImageButton restartButton = findViewById(R.id.size_compat_restart_button); - final ColorStateList color = ColorStateList.valueOf(Color.LTGRAY); - final GradientDrawable mask = new GradientDrawable(); - mask.setShape(GradientDrawable.OVAL); - mask.setColor(color); - restartButton.setBackground(new RippleDrawable(color, null /* content */, mask)); - restartButton.setOnClickListener(this); - restartButton.setOnLongClickListener(this); - } - - @Override - public void onClick(View v) { - mLayout.onRestartButtonClicked(); - } - - @Override - public boolean onLongClick(View v) { - mLayout.onRestartButtonLongClicked(); - return true; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java deleted file mode 100644 index 20021ebea834..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUILayout.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; -import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; - -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.os.Binder; -import android.util.Log; -import android.view.SurfaceControl; -import android.view.View; -import android.view.WindowManager; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.wm.shell.R; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.SyncTransactionQueue; - -/** - * Records and handles layout of size compat UI on a task with size compat activity. Helps to - * calculate proper bounds when configuration or UI position changes. - */ -class SizeCompatUILayout { - private static final String TAG = "SizeCompatUILayout"; - - final SyncTransactionQueue mSyncQueue; - private final SizeCompatUIController.SizeCompatUICallback mCallback; - private Context mContext; - private Configuration mTaskConfig; - private final int mDisplayId; - private final int mTaskId; - private ShellTaskOrganizer.TaskListener mTaskListener; - private DisplayLayout mDisplayLayout; - - @VisibleForTesting - final SizeCompatUIWindowManager mButtonWindowManager; - @VisibleForTesting - @Nullable - SizeCompatUIWindowManager mHintWindowManager; - @VisibleForTesting - @Nullable - SizeCompatRestartButton mButton; - @VisibleForTesting - @Nullable - SizeCompatHintPopup mHint; - final int mButtonSize; - final int mPopupOffsetX; - final int mPopupOffsetY; - boolean mShouldShowHint; - - SizeCompatUILayout(SyncTransactionQueue syncQueue, - SizeCompatUIController.SizeCompatUICallback callback, Context context, - Configuration taskConfig, int taskId, ShellTaskOrganizer.TaskListener taskListener, - DisplayLayout displayLayout, boolean hasShownHint) { - mSyncQueue = syncQueue; - mCallback = callback; - mContext = context.createConfigurationContext(taskConfig); - mTaskConfig = taskConfig; - mDisplayId = mContext.getDisplayId(); - mTaskId = taskId; - mTaskListener = taskListener; - mDisplayLayout = displayLayout; - mShouldShowHint = !hasShownHint; - mButtonWindowManager = new SizeCompatUIWindowManager(mContext, taskConfig, this); - - mButtonSize = - mContext.getResources().getDimensionPixelSize(R.dimen.size_compat_button_size); - mPopupOffsetX = mButtonSize / 4; - mPopupOffsetY = mButtonSize; - } - - /** Creates the activity restart button window. */ - void createSizeCompatButton(boolean isImeShowing) { - if (isImeShowing || mButton != null) { - // When ime is showing, wait until ime is dismiss to create UI. - return; - } - mButton = mButtonWindowManager.createSizeCompatButton(); - updateButtonSurfacePosition(); - - if (mShouldShowHint) { - // Only show by default for the first time. - mShouldShowHint = false; - createSizeCompatHint(); - } - } - - /** Creates the restart button hint window. */ - private void createSizeCompatHint() { - if (mHint != null) { - // Hint already shown. - return; - } - mHintWindowManager = createHintWindowManager(); - mHint = mHintWindowManager.createSizeCompatHint(); - updateHintSurfacePosition(); - } - - @VisibleForTesting - SizeCompatUIWindowManager createHintWindowManager() { - return new SizeCompatUIWindowManager(mContext, mTaskConfig, this); - } - - /** Dismisses the hint window. */ - void dismissHint() { - mHint = null; - if (mHintWindowManager != null) { - mHintWindowManager.release(); - mHintWindowManager = null; - } - } - - /** Releases the UI windows. */ - void release() { - dismissHint(); - mButton = null; - mButtonWindowManager.release(); - } - - /** Called when size compat info changed. */ - void updateSizeCompatInfo(Configuration taskConfig, - ShellTaskOrganizer.TaskListener taskListener, boolean isImeShowing) { - final Configuration prevTaskConfig = mTaskConfig; - final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener; - mTaskConfig = taskConfig; - mTaskListener = taskListener; - - // Update configuration. - mContext = mContext.createConfigurationContext(taskConfig); - mButtonWindowManager.setConfiguration(taskConfig); - if (mHintWindowManager != null) { - mHintWindowManager.setConfiguration(taskConfig); - } - - if (mButton == null || prevTaskListener != taskListener) { - // TaskListener changed, recreate the button for new surface parent. - release(); - createSizeCompatButton(isImeShowing); - return; - } - - if (!taskConfig.windowConfiguration.getBounds() - .equals(prevTaskConfig.windowConfiguration.getBounds())) { - // Reposition the UI surfaces. - updateButtonSurfacePosition(); - updateHintSurfacePosition(); - } - - if (taskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection()) { - // Update layout for RTL. - mButton.setLayoutDirection(taskConfig.getLayoutDirection()); - updateButtonSurfacePosition(); - if (mHint != null) { - mHint.setLayoutDirection(taskConfig.getLayoutDirection()); - updateHintSurfacePosition(); - } - } - } - - /** Called when display layout changed. */ - void updateDisplayLayout(DisplayLayout displayLayout) { - if (displayLayout == mDisplayLayout) { - return; - } - - final Rect prevStableBounds = new Rect(); - final Rect curStableBounds = new Rect(); - mDisplayLayout.getStableBounds(prevStableBounds); - displayLayout.getStableBounds(curStableBounds); - mDisplayLayout = displayLayout; - if (!prevStableBounds.equals(curStableBounds)) { - // Stable bounds changed, update UI surface positions. - updateButtonSurfacePosition(); - updateHintSurfacePosition(); - } - } - - /** Called when IME visibility changed. */ - void updateImeVisibility(boolean isImeShowing) { - if (mButton == null) { - // Button may not be created because ime is previous showing. - createSizeCompatButton(isImeShowing); - return; - } - - // Hide size compat UIs when IME is showing. - final int newVisibility = isImeShowing ? View.GONE : View.VISIBLE; - if (mButton.getVisibility() != newVisibility) { - mButton.setVisibility(newVisibility); - } - if (mHint != null && mHint.getVisibility() != newVisibility) { - mHint.setVisibility(newVisibility); - } - } - - /** Gets the layout params for restart button. */ - WindowManager.LayoutParams getButtonWindowLayoutParams() { - final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams( - // Cannot be wrap_content as this determines the actual window size - mButtonSize, mButtonSize, - TYPE_APPLICATION_OVERLAY, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, - PixelFormat.TRANSLUCENT); - winParams.token = new Binder(); - winParams.setTitle(SizeCompatRestartButton.class.getSimpleName() + getTaskId()); - winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; - return winParams; - } - - /** Gets the layout params for hint popup. */ - WindowManager.LayoutParams getHintWindowLayoutParams(SizeCompatHintPopup hint) { - final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams( - // Cannot be wrap_content as this determines the actual window size - hint.getMeasuredWidth(), hint.getMeasuredHeight(), - TYPE_APPLICATION_OVERLAY, - FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL, - PixelFormat.TRANSLUCENT); - winParams.token = new Binder(); - winParams.setTitle(SizeCompatHintPopup.class.getSimpleName() + getTaskId()); - winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; - winParams.windowAnimations = android.R.style.Animation_InputMethod; - return winParams; - } - - /** Called when it is ready to be placed size compat UI surface. */ - void attachToParentSurface(SurfaceControl.Builder b) { - mTaskListener.attachChildSurfaceToTask(mTaskId, b); - } - - /** Called when the restart button is clicked. */ - void onRestartButtonClicked() { - mCallback.onSizeCompatRestartButtonClicked(mTaskId); - } - - /** Called when the restart button is long clicked. */ - void onRestartButtonLongClicked() { - createSizeCompatHint(); - } - - @VisibleForTesting - void updateButtonSurfacePosition() { - if (mButton == null || mButtonWindowManager.getSurfaceControl() == null) { - return; - } - final SurfaceControl leash = mButtonWindowManager.getSurfaceControl(); - - // Use stable bounds to prevent the button from overlapping with system bars. - final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds(); - final Rect stableBounds = new Rect(); - mDisplayLayout.getStableBounds(stableBounds); - stableBounds.intersect(taskBounds); - - // Position of the button in the container coordinate. - final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL - ? stableBounds.left - taskBounds.left - : stableBounds.right - taskBounds.left - mButtonSize; - final int positionY = stableBounds.bottom - taskBounds.top - mButtonSize; - - updateSurfacePosition(leash, positionX, positionY); - } - - void updateHintSurfacePosition() { - if (mHint == null || mHintWindowManager == null - || mHintWindowManager.getSurfaceControl() == null) { - return; - } - final SurfaceControl leash = mHintWindowManager.getSurfaceControl(); - - // Use stable bounds to prevent the hint from overlapping with system bars. - final Rect taskBounds = mTaskConfig.windowConfiguration.getBounds(); - final Rect stableBounds = new Rect(); - mDisplayLayout.getStableBounds(stableBounds); - stableBounds.intersect(taskBounds); - - // Position of the hint in the container coordinate. - final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL - ? stableBounds.left - taskBounds.left + mPopupOffsetX - : stableBounds.right - taskBounds.left - mPopupOffsetX - mHint.getMeasuredWidth(); - final int positionY = - stableBounds.bottom - taskBounds.top - mPopupOffsetY - mHint.getMeasuredHeight(); - - updateSurfacePosition(leash, positionX, positionY); - } - - private void updateSurfacePosition(SurfaceControl leash, int positionX, int positionY) { - mSyncQueue.runInSync(t -> { - if (!leash.isValid()) { - Log.w(TAG, "The leash has been released."); - return; - } - t.setPosition(leash, positionX, positionY); - // The size compat UI should be the topmost child of the Task in case there can be more - // than one children. - t.setLayer(leash, Integer.MAX_VALUE); - }); - } - - int getDisplayId() { - return mDisplayId; - } - - int getTaskId() { - return mTaskId; - } - - private int getLayoutDirection() { - return mContext.getResources().getConfiguration().getLayoutDirection(); - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java deleted file mode 100644 index 82f69c3e2985..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sizecompatui/SizeCompatUIWindowManager.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import android.annotation.Nullable; -import android.content.Context; -import android.content.res.Configuration; -import android.view.IWindow; -import android.view.LayoutInflater; -import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.SurfaceSession; -import android.view.View; -import android.view.WindowlessWindowManager; - -import com.android.wm.shell.R; - -/** - * Holds view hierarchy of a root surface and helps to inflate {@link SizeCompatRestartButton} or - * {@link SizeCompatHintPopup}. - */ -class SizeCompatUIWindowManager extends WindowlessWindowManager { - - private Context mContext; - private final SizeCompatUILayout mLayout; - - @Nullable - private SurfaceControlViewHost mViewHost; - @Nullable - private SurfaceControl mLeash; - - SizeCompatUIWindowManager(Context context, Configuration config, SizeCompatUILayout layout) { - super(config, null /* rootSurface */, null /* hostInputToken */); - mContext = context; - mLayout = layout; - } - - @Override - public void setConfiguration(Configuration configuration) { - super.setConfiguration(configuration); - mContext = mContext.createConfigurationContext(configuration); - } - - @Override - protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { - // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later. - final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) - .setContainerLayer() - .setName("SizeCompatUILeash") - .setHidden(false) - .setCallsite("SizeCompatUIWindowManager#attachToParentSurface"); - mLayout.attachToParentSurface(builder); - mLeash = builder.build(); - b.setParent(mLeash); - } - - /** Inflates {@link SizeCompatRestartButton} on to the root surface. */ - SizeCompatRestartButton createSizeCompatButton() { - if (mViewHost != null) { - throw new IllegalStateException( - "A UI has already been created with this window manager."); - } - - mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); - - final SizeCompatRestartButton button = (SizeCompatRestartButton) - LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null); - button.inject(mLayout); - mViewHost.setView(button, mLayout.getButtonWindowLayoutParams()); - return button; - } - - /** Inflates {@link SizeCompatHintPopup} on to the root surface. */ - SizeCompatHintPopup createSizeCompatHint() { - if (mViewHost != null) { - throw new IllegalStateException( - "A UI has already been created with this window manager."); - } - - mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); - - final SizeCompatHintPopup hint = (SizeCompatHintPopup) - LayoutInflater.from(mContext).inflate(R.layout.size_compat_mode_hint, null); - // Measure how big the hint is. - hint.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - hint.inject(mLayout); - mViewHost.setView(hint, mLayout.getHintWindowLayoutParams(hint)); - return hint; - } - - /** Releases the surface control and tears down the view hierarchy. */ - void release() { - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - - if (mLeash != null) { - final SurfaceControl leash = mLeash; - mLayout.mSyncQueue.runInSync(t -> t.remove(leash)); - mLeash = null; - } - } - - /** - * Gets {@link SurfaceControl} of the surface holding size compat UI view. @return {@code null} - * if not feasible. - */ - @Nullable - SurfaceControl getSurfaceControl() { - return mLeash; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index 8f0892fdcbba..3cfa541c1c86 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -20,7 +20,9 @@ import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; import android.os.UserHandle; -import android.window.IRemoteTransition; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.window.RemoteTransition; import com.android.wm.shell.splitscreen.ISplitScreenListener; @@ -50,9 +52,10 @@ interface ISplitScreen { oneway void removeFromSideStage(int taskId) = 4; /** - * Removes the split-screen stages. + * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID + * to indicate leaving no top task after leaving split-screen. */ - oneway void exitSplitScreen() = 5; + oneway void exitSplitScreen(int toTopTaskId) = 5; /** * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible. @@ -62,24 +65,40 @@ interface ISplitScreen { /** * Starts a task in a stage. */ - oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7; + oneway void startTask(int taskId, int position, in Bundle options) = 7; /** * Starts a shortcut in a stage. */ - oneway void startShortcut(String packageName, String shortcutId, int stage, int position, + oneway void startShortcut(String packageName, String shortcutId, int position, in Bundle options, in UserHandle user) = 8; /** * Starts an activity in a stage. */ - oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage, - int position, in Bundle options) = 9; + oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int position, + in Bundle options) = 9; /** - * Starts tasks simultaneously in one transition. The first task in the list will be in the - * main-stage and on the left/top. + * Starts tasks simultaneously in one transition. */ oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, - in Bundle sideOptions, int sidePosition, in IRemoteTransition remoteTransition) = 10; + in Bundle sideOptions, int sidePosition, float splitRatio, + in RemoteTransition remoteTransition) = 10; + + /** + * Version of startTasks using legacy transition system. + */ + oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, + int sideTaskId, in Bundle sideOptions, int sidePosition, + float splitRatio, in RemoteAnimationAdapter adapter) = 11; + + /** + * Blocking call that notifies and gets additional split-screen targets when entering + * recents (for example: the dividerBar). + * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled). + * @param appTargets apps that will be re-parented to display area + */ + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + in RemoteAnimationTarget[] appTargets) = 12; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java index d0998eb57633..082fe9205be8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java @@ -16,13 +16,14 @@ package com.android.wm.shell.splitscreen; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; - +import android.annotation.Nullable; +import android.content.Context; import android.graphics.Rect; import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; @@ -36,35 +37,35 @@ class MainStage extends StageTaskListener { private boolean mIsActive = false; - MainStage(ShellTaskOrganizer taskOrganizer, int displayId, + MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession) { - super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession); + SurfaceSession surfaceSession, IconProvider iconProvider, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider, + stageTaskUnfoldController); } boolean isActive() { return mIsActive; } - void activate(Rect rootBounds, WindowContainerTransaction wct) { + void activate(Rect rootBounds, WindowContainerTransaction wct, boolean includingTopTask) { if (mIsActive) return; final WindowContainerToken rootToken = mRootTaskInfo.token; wct.setBounds(rootToken, rootBounds) - .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW) - .setLaunchRoot( - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES) - .reparentTasks( - null /* currentParent */, - rootToken, - CONTROLLED_WINDOWING_MODES, - CONTROLLED_ACTIVITY_TYPES, - true /* onTop */) // Moving the root task to top after the child tasks were re-parented , or the root // task cannot be visible and focused. .reorder(rootToken, true /* onTop */); + if (includingTopTask) { + wct.reparentTasks( + null /* currentParent */, + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + true /* onTop */, + true /* reparentTopOnly */); + } mIsActive = true; } @@ -79,11 +80,7 @@ class MainStage extends StageTaskListener { if (mRootTaskInfo == null) return; final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setLaunchRoot( - rootToken, - null, - null) - .reparentTasks( + wct.reparentTasks( rootToken, null /* newParent */, CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, @@ -93,9 +90,4 @@ class MainStage extends StageTaskListener { // all its tasks. .reorder(rootToken, false /* onTop */); } - - void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) { - wct.setBounds(mRootTaskInfo.token, bounds) - .setWindowingMode(mRootTaskInfo.token, windowingMode); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OWNERS new file mode 100644 index 000000000000..7237d2bde39f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-modules splitscreen owner +chenghsiuchang@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java index 82f95a4f32ea..122fc9f5f780 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SideStage.java @@ -16,37 +16,32 @@ package com.android.wm.shell.splitscreen; +import android.annotation.Nullable; import android.app.ActivityManager; -import android.graphics.Rect; +import android.content.Context; import android.view.SurfaceSession; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; /** * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up * here. All other task are launch in the {@link MainStage}. + * * @see StageCoordinator */ class SideStage extends StageTaskListener { private static final String TAG = SideStage.class.getSimpleName(); - SideStage(ShellTaskOrganizer taskOrganizer, int displayId, + SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession) { - super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession); - } - - void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, - WindowContainerTransaction wct) { - final WindowContainerToken rootToken = mRootTaskInfo.token; - wct.setBounds(rootToken, rootBounds) - .reparent(task.token, rootToken, true /* onTop*/) - // Moving the root task to top after the child tasks were repareted , or the root - // task cannot be visible and focused. - .reorder(rootToken, true /* onTop */); + SurfaceSession surfaceSession, IconProvider iconProvider, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(context, taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, iconProvider, + stageTaskUnfoldController); } boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 002bfb6e429f..a91dfe1c13e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -17,9 +17,12 @@ package com.android.wm.shell.splitscreen; import android.annotation.IntDef; +import android.annotation.NonNull; import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; + +import java.util.concurrent.Executor; /** * Interface to engage split-screen feature. @@ -53,10 +56,18 @@ public interface SplitScreen { /** Callback interface for listening to changes in a split-screen stage. */ interface SplitScreenListener { - void onStagePositionChanged(@StageType int stage, @SplitPosition int position); - void onTaskStageChanged(int taskId, @StageType int stage, boolean visible); + default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {} + default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {} + default void onSplitVisibilityChanged(boolean visible) {} } + /** Registers listener that gets split screen callback. */ + void registerSplitScreenListener(@NonNull SplitScreenListener listener, + @NonNull Executor executor); + + /** Unregisters listener that gets split screen callback. */ + void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); + /** * Returns a binder that can be passed to an external process to manipulate SplitScreen. */ @@ -64,6 +75,24 @@ public interface SplitScreen { return null; } + /** + * Called when the keyguard occluded state changes. + * @param occluded Indicates if the keyguard is now occluded. + */ + void onKeyguardOccludedChanged(boolean occluded); + + /** + * Called when the visibility of the keyguard changes. + * @param showing Indicates if the keyguard is now visible. + */ + void onKeyguardVisibilityChanged(boolean showing); + + /** Called when device waking up finished. */ + void onFinishedWakingUp(); + + /** Called when device going to sleep finished. */ + void onFinishedGoingToSleep(); + /** Get a string representation of a stage type */ static String stageTypeToString(@StageType int stage) { switch (stage) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 9a457b5fd88e..8b87df44c52c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -16,13 +16,14 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityManager.START_SUCCESS; +import static android.app.ActivityManager.START_TASK_TO_FRONT; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.RemoteAnimationTarget.MODE_OPENING; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; @@ -35,40 +36,88 @@ import android.content.Intent; import android.content.pm.LauncherApps; import android.graphics.Rect; import android.os.Bundle; -import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; +import android.util.ArrayMap; import android.util.Slog; -import android.window.IRemoteTransition; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; +import android.window.RemoteTransition; +import android.window.WindowContainerTransaction; import androidx.annotation.BinderThread; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.logging.InstanceId; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ExternalThread; -import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.draganddrop.DragAndDropPolicy; -import com.android.wm.shell.splitscreen.ISplitScreenListener; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.SplitScreen.StageType; +import com.android.wm.shell.transition.LegacyTransitions; import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.inject.Provider; /** * Class manages split-screen multitasking mode and implements the main interface * {@link SplitScreen}. + * * @see StageCoordinator */ +// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. public class SplitScreenController implements DragAndDropPolicy.Starter, RemoteCallable<SplitScreenController> { private static final String TAG = SplitScreenController.class.getSimpleName(); + static final int EXIT_REASON_UNKNOWN = 0; + static final int EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW = 1; + static final int EXIT_REASON_APP_FINISHED = 2; + static final int EXIT_REASON_DEVICE_FOLDED = 3; + static final int EXIT_REASON_DRAG_DIVIDER = 4; + static final int EXIT_REASON_RETURN_HOME = 5; + static final int EXIT_REASON_ROOT_TASK_VANISHED = 6; + static final int EXIT_REASON_SCREEN_LOCKED = 7; + static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8; + static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9; + @IntDef(value = { + EXIT_REASON_UNKNOWN, + EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW, + EXIT_REASON_APP_FINISHED, + EXIT_REASON_DEVICE_FOLDED, + EXIT_REASON_DRAG_DIVIDER, + EXIT_REASON_RETURN_HOME, + EXIT_REASON_ROOT_TASK_VANISHED, + EXIT_REASON_SCREEN_LOCKED, + EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP, + EXIT_REASON_CHILD_TASK_ENTER_PIP, + }) + @Retention(RetentionPolicy.SOURCE) + @interface ExitReason{} + private final ShellTaskOrganizer mTaskOrganizer; private final SyncTransactionQueue mSyncQueue; private final Context mContext; @@ -76,8 +125,13 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, private final ShellExecutor mMainExecutor; private final SplitScreenImpl mImpl = new SplitScreenImpl(); private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private final Transitions mTransitions; private final TransactionPool mTransactionPool; + private final SplitscreenEventLogger mLogger; + private final IconProvider mIconProvider; + private final Optional<RecentTasksController> mRecentTasksOptional; + private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider; private StageCoordinator mStageCoordinator; @@ -85,15 +139,23 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, SyncTransactionQueue syncQueue, Context context, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellExecutor mainExecutor, DisplayImeController displayImeController, - Transitions transitions, TransactionPool transactionPool) { + DisplayInsetsController displayInsetsController, + Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, + Optional<RecentTasksController> recentTasks, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mContext = context; mRootTDAOrganizer = rootTDAOrganizer; mMainExecutor = mainExecutor; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mTransitions = transitions; mTransactionPool = transactionPool; + mUnfoldControllerProvider = unfoldControllerProvider; + mLogger = new SplitscreenEventLogger(); + mIconProvider = iconProvider; + mRecentTasksOptional = recentTasks; } public SplitScreen asSplitScreen() { @@ -114,8 +176,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, if (mStageCoordinator == null) { // TODO: Multi-display mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, - mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, mTransitions, - mTransactionPool); + mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, + mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, + mIconProvider, mRecentTasksOptional, mUnfoldControllerProvider); } } @@ -123,17 +186,36 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, return mStageCoordinator.isSplitScreenVisible(); } + @Nullable + public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) { + if (isSplitScreenVisible()) { + int taskId = mStageCoordinator.getTaskId(splitPosition); + return mTaskOrganizer.getRunningTaskInfo(taskId); + } + return null; + } + + public boolean isTaskInSplitScreen(int taskId) { + return isSplitScreenVisible() + && mStageCoordinator.getStageOfTask(taskId) != STAGE_TYPE_UNDEFINED; + } + + public @SplitPosition int getSplitPosition(int taskId) { + return mStageCoordinator.getSplitPosition(taskId); + } + public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) { + return moveToStage(taskId, STAGE_TYPE_SIDE, sideStagePosition, + new WindowContainerTransaction()); + } + + private boolean moveToStage(int taskId, @StageType int stageType, + @SplitPosition int stagePosition, WindowContainerTransaction wct) { final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); if (task == null) { throw new IllegalArgumentException("Unknown taskId" + taskId); } - return moveToSideStage(task, sideStagePosition); - } - - public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SplitPosition int sideStagePosition) { - return mStageCoordinator.moveToSideStage(task, sideStagePosition); + return mStageCoordinator.moveToStage(task, stageType, stagePosition, wct); } public boolean removeFromSideStage(int taskId) { @@ -141,7 +223,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void setSideStagePosition(@SplitPosition int sideStagePosition) { - mStageCoordinator.setSideStagePosition(sideStagePosition); + mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */); } public void setSideStageVisibility(boolean visible) { @@ -149,12 +231,34 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void enterSplitScreen(int taskId, boolean leftOrTop) { - moveToSideStage(taskId, - leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); + enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction()); + } + + public void enterSplitScreen(int taskId, boolean leftOrTop, WindowContainerTransaction wct) { + final int stageType = isSplitScreenVisible() ? STAGE_TYPE_UNDEFINED : STAGE_TYPE_SIDE; + final int stagePosition = + leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT; + moveToStage(taskId, stageType, stagePosition, wct); + } + + public void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { + mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); + } + + public void onKeyguardOccludedChanged(boolean occluded) { + mStageCoordinator.onKeyguardOccludedChanged(occluded); } - public void exitSplitScreen() { - mStageCoordinator.exitSplitScreen(); + public void onKeyguardVisibilityChanged(boolean showing) { + mStageCoordinator.onKeyguardVisibilityChanged(showing); + } + + public void onFinishedWakingUp() { + mStageCoordinator.onFinishedWakingUp(); + } + + public void onFinishedGoingToSleep() { + mStageCoordinator.onFinishedGoingToSleep(); } public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { @@ -173,93 +277,151 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.unregisterSplitScreenListener(listener); } - public void startTask(int taskId, @SplitScreen.StageType int stage, - @SplitPosition int position, @Nullable Bundle options) { - options = resolveStartStage(stage, position, options); + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, + null /* wct */); try { - ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictChildTasks(position, evictWct); + final int result = + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + if (result == START_SUCCESS || result == START_TASK_TO_FRONT) { + mSyncQueue.queue(evictWct); + } } catch (RemoteException e) { Slog.e(TAG, "Failed to launch task", e); } } - public void startShortcut(String packageName, String shortcutId, - @SplitScreen.StageType int stage, @SplitPosition int position, + public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, @Nullable Bundle options, UserHandle user) { - options = resolveStartStage(stage, position, options); + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, + null /* wct */); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictChildTasks(position, evictWct); try { LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, options, user); + mSyncQueue.queue(evictWct); } catch (ActivityNotFoundException e) { Slog.e(TAG, "Failed to launch shortcut", e); } } - public void startIntent(PendingIntent intent, Intent fillInIntent, - @SplitScreen.StageType int stage, @SplitPosition int position, + public void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { - options = resolveStartStage(stage, position, options); - - try { - intent.send(mContext, 0, fillInIntent, null, null, null, options); - } catch (PendingIntent.CanceledException e) { - Slog.e(TAG, "Failed to launch activity", e); + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, position, options); + return; } + mStageCoordinator.startIntent(intent, fillInIntent, STAGE_TYPE_UNDEFINED, position, options, + null /* remote */); } - private Bundle resolveStartStage(@SplitScreen.StageType int stage, + private void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options) { - switch (stage) { - case STAGE_TYPE_UNDEFINED: { - // Use the stage of the specified position is valid. - if (position != SPLIT_POSITION_UNDEFINED) { - if (position == mStageCoordinator.getSideStagePosition()) { - options = resolveStartStage(STAGE_TYPE_SIDE, position, options); - } else { - options = resolveStartStage(STAGE_TYPE_MAIN, position, options); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + mStageCoordinator.prepareEvictChildTasks(position, evictWct); + + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + mStageCoordinator.updateSurfaceBounds(null /* layout */, t); + + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } } - } else { - // Exit split-screen and launch fullscreen since stage wasn't specified. - mStageCoordinator.exitSplitScreen(); } - break; - } - case STAGE_TYPE_SIDE: { - if (position != SPLIT_POSITION_UNDEFINED) { - mStageCoordinator.setSideStagePosition(position); - } else { - position = mStageCoordinator.getSideStagePosition(); - } - if (options == null) { - options = new Bundle(); - } - mStageCoordinator.updateActivityOptions(options, position); - break; - } - case STAGE_TYPE_MAIN: { - if (position != SPLIT_POSITION_UNDEFINED) { - // Set the side stage opposite of what we want to the main stage. - final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; - mStageCoordinator.setSideStagePosition(sideStagePosition); - } else { - position = mStageCoordinator.getMainStagePosition(); - } - if (options == null) { - options = new Bundle(); + + t.apply(); + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); + } } - mStageCoordinator.updateActivityOptions(options, position); - break; + + mSyncQueue.queue(evictWct); } - default: - throw new IllegalArgumentException("Unknown stage=" + stage); + }; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { + if (apps.length < 2) return null; + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("RecentsAnimationSplitTasks") + .setHidden(false) + .setCallsite("SplitScreenController#onGoingtoRecentsLegacy"); + mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder); + SurfaceControl sc = builder.build(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + + // Ensure that we order these in the parent in the right z-order as their previous order + Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex); + int layer = 1; + for (RemoteAnimationTarget appTarget : apps) { + transaction.reparent(appTarget.leash, sc); + transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, + appTarget.screenSpaceBounds.top); + transaction.setLayer(appTarget.leash, layer++); } + transaction.apply(); + transaction.close(); + return new RemoteAnimationTarget[]{mStageCoordinator.getDividerBarLegacyTarget()}; + } - return options; + /** + * Sets drag info to be logged when splitscreen is entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mStageCoordinator.logOnDroppedToSplit(position, dragSessionId); + } + + /** + * Return the {@param exitReason} as a string. + */ + public static String exitReasonToString(int exitReason) { + switch (exitReason) { + case EXIT_REASON_UNKNOWN: + return "UNKNOWN_EXIT"; + case EXIT_REASON_DRAG_DIVIDER: + return "DRAG_DIVIDER"; + case EXIT_REASON_RETURN_HOME: + return "RETURN_HOME"; + case EXIT_REASON_SCREEN_LOCKED: + return "SCREEN_LOCKED"; + case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: + return "SCREEN_LOCKED_SHOW_ON_TOP"; + case EXIT_REASON_DEVICE_FOLDED: + return "DEVICE_FOLDED"; + case EXIT_REASON_ROOT_TASK_VANISHED: + return "ROOT_TASK_VANISHED"; + case EXIT_REASON_APP_FINISHED: + return "APP_FINISHED"; + case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW: + return "APP_DOES_NOT_SUPPORT_MULTIWINDOW"; + case EXIT_REASON_CHILD_TASK_ENTER_PIP: + return "CHILD_TASK_ENTER_PIP"; + default: + return "unknown reason, reason int = " + exitReason; + } } public void dump(@NonNull PrintWriter pw, String prefix) { @@ -275,6 +437,38 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @ExternalThread private class SplitScreenImpl implements SplitScreen { private ISplitScreenImpl mISplitScreen; + private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); + private final SplitScreen.SplitScreenListener mListener = new SplitScreenListener() { + @Override + public void onStagePositionChanged(int stage, int position) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onStagePositionChanged(stage, position); + }); + } + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible); + }); + } + } + + @Override + public void onSplitVisibilityChanged(boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onSplitVisibilityChanged(visible); + }); + } + } + }; @Override public ISplitScreen createExternalInterface() { @@ -284,6 +478,62 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); return mISplitScreen; } + + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardOccludedChanged(occluded); + }); + } + + @Override + public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { + if (mExecutors.containsKey(listener)) return; + + mMainExecutor.execute(() -> { + if (mExecutors.size() == 0) { + SplitScreenController.this.registerSplitScreenListener(mListener); + } + + mExecutors.put(listener, executor); + }); + + executor.execute(() -> { + mStageCoordinator.sendStatusToListener(listener); + }); + } + + @Override + public void unregisterSplitScreenListener(SplitScreenListener listener) { + mMainExecutor.execute(() -> { + mExecutors.remove(listener); + + if (mExecutors.size() == 0) { + SplitScreenController.this.unregisterSplitScreenListener(mListener); + } + }); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardVisibilityChanged(showing); + }); + } + + @Override + public void onFinishedWakingUp() { + mMainExecutor.execute(() -> { + SplitScreenController.this.onFinishedWakingUp(); + }); + } + + @Override + public void onFinishedGoingToSleep() { + mMainExecutor.execute(() -> { + SplitScreenController.this.onFinishedGoingToSleep(); + }); + } } /** @@ -292,46 +542,26 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @BinderThread private static class ISplitScreenImpl extends ISplitScreen.Stub { private SplitScreenController mController; - private ISplitScreenListener mListener; + private final SingleInstanceRemoteListener<SplitScreenController, + ISplitScreenListener> mListener; private final SplitScreen.SplitScreenListener mSplitScreenListener = new SplitScreen.SplitScreenListener() { @Override public void onStagePositionChanged(int stage, int position) { - try { - if (mListener != null) { - mListener.onStagePositionChanged(stage, position); - } - } catch (RemoteException e) { - Slog.e(TAG, "onStagePositionChanged", e); - } + mListener.call(l -> l.onStagePositionChanged(stage, position)); } @Override public void onTaskStageChanged(int taskId, int stage, boolean visible) { - try { - if (mListener != null) { - mListener.onTaskStageChanged(taskId, stage, visible); - } - } catch (RemoteException e) { - Slog.e(TAG, "onTaskStageChanged", e); - } - } - }; - private final IBinder.DeathRecipient mListenerDeathRecipient = - new IBinder.DeathRecipient() { - @Override - @BinderThread - public void binderDied() { - final SplitScreenController controller = mController; - controller.getRemoteCallExecutor().execute(() -> { - mListener = null; - controller.unregisterSplitScreenListener(mSplitScreenListener); - }); + mListener.call(l -> l.onTaskStageChanged(taskId, stage, visible)); } }; public ISplitScreenImpl(SplitScreenController controller) { mController = controller; + mListener = new SingleInstanceRemoteListener<>(controller, + c -> c.registerSplitScreenListener(mSplitScreenListener), + c -> c.unregisterSplitScreenListener(mSplitScreenListener)); } /** @@ -344,43 +574,20 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, @Override public void registerSplitScreenListener(ISplitScreenListener listener) { executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener", - (controller) -> { - if (mListener != null) { - mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } - if (listener != null) { - try { - listener.asBinder().linkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to link to death"); - return; - } - } - mListener = listener; - controller.registerSplitScreenListener(mSplitScreenListener); - }); + (controller) -> mListener.register(listener)); } @Override public void unregisterSplitScreenListener(ISplitScreenListener listener) { executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener", - (controller) -> { - if (mListener != null) { - mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } - mListener = null; - controller.unregisterSplitScreenListener(mSplitScreenListener); - }); + (controller) -> mListener.unregister()); } @Override - public void exitSplitScreen() { + public void exitSplitScreen(int toTopTaskId) { executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", (controller) -> { - controller.exitSplitScreen(); + controller.exitSplitScreen(toTopTaskId, EXIT_REASON_UNKNOWN); }); } @@ -409,40 +616,59 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } @Override - public void startTask(int taskId, int stage, int position, @Nullable Bundle options) { + public void startTask(int taskId, int position, @Nullable Bundle options) { executeRemoteCallWithTaskPermission(mController, "startTask", (controller) -> { - controller.startTask(taskId, stage, position, options); + controller.startTask(taskId, position, options); }); } @Override + public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + float splitRatio, RemoteAnimationAdapter adapter) { + executeRemoteCallWithTaskPermission(mController, "startTasks", + (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( + mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, + splitRatio, adapter)); + } + + @Override public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, @Nullable Bundle sideOptions, - @SplitPosition int sidePosition, - @Nullable IRemoteTransition remoteTransition) { + @SplitPosition int sidePosition, float splitRatio, + @Nullable RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mController, "startTasks", (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, - sideTaskId, sideOptions, sidePosition, remoteTransition)); + sideTaskId, sideOptions, sidePosition, splitRatio, remoteTransition)); } @Override - public void startShortcut(String packageName, String shortcutId, int stage, int position, + public void startShortcut(String packageName, String shortcutId, int position, @Nullable Bundle options, UserHandle user) { executeRemoteCallWithTaskPermission(mController, "startShortcut", (controller) -> { - controller.startShortcut(packageName, shortcutId, stage, position, - options, user); + controller.startShortcut(packageName, shortcutId, position, options, user); }); } @Override - public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position, + public void startIntent(PendingIntent intent, Intent fillInIntent, int position, @Nullable Bundle options) { executeRemoteCallWithTaskPermission(mController, "startIntent", (controller) -> { - controller.startIntent(intent, fillInIntent, stage, position, options); + controller.startIntent(intent, fillInIntent, position, options); }); } + + @Override + public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + RemoteAnimationTarget[] apps) { + final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; + executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", + (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), + true /* blocking */); + return out[0]; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index c37789ecbc9d..86e7b0e4cb7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -35,7 +35,7 @@ import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; import android.view.WindowManager; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -84,17 +84,19 @@ class SplitScreenTransitions { } void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { mFinishCallback = finishCallback; mAnimatingTransition = transition; if (mRemoteHandler != null) { - mRemoteHandler.startAnimation(transition, info, t, mRemoteFinishCB); + mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, + mRemoteFinishCB); mRemoteHandler = null; return; } - playInternalAnimation(transition, info, t, mainRoot, sideRoot); + playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); } private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -167,7 +169,7 @@ class SplitScreenTransitions { /** Starts a transition to enter split with a remote transition animator. */ IBinder startEnterTransition(@WindowManager.TransitionType int transitType, - @NonNull WindowContainerTransaction wct, @Nullable IRemoteTransition remoteTransition, + @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, @NonNull Transitions.TransitionHandler handler) { if (remoteTransition != null) { // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java new file mode 100644 index 000000000000..3e7a1004ed7a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2021 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.splitscreen; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN; + +import android.util.Slog; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; + +/** + * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent + */ +public class SplitscreenEventLogger { + + // Used to generate instance ids for this drag if one is not provided + private final InstanceIdSequence mIdSequence; + + // The instance id for the current splitscreen session (from start to end) + private InstanceId mLoggerSessionId; + + // Drag info + private @SplitPosition int mDragEnterPosition; + private InstanceId mDragEnterSessionId; + + // For deduping async events + private int mLastMainStagePosition = -1; + private int mLastMainStageUid = -1; + private int mLastSideStagePosition = -1; + private int mLastSideStageUid = -1; + private float mLastSplitRatio = -1f; + + public SplitscreenEventLogger() { + mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); + } + + /** + * Return whether a splitscreen session has started. + */ + public boolean hasStartedSession() { + return mLoggerSessionId != null; + } + + /** + * May be called before logEnter() to indicate that the session was started from a drag. + */ + public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { + mDragEnterPosition = position; + mDragEnterSessionId = dragSessionId; + } + + /** + * Logs when the user enters splitscreen. + */ + public void logEnter(float splitRatio, + @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + mLoggerSessionId = mIdSequence.newInstanceId(); + int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED + ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) + : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + updateSplitRatioState(splitRatio); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER, + enterReason, + 0 /* exitReason */, + splitRatio, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mLoggerSessionId.getId()); + } + + /** + * Returns the framework logging constant given a splitscreen exit reason. + */ + private int getLoggerExitReason(@ExitReason int exitReason) { + switch (exitReason) { + case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW: + return SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; + case EXIT_REASON_APP_FINISHED: + return SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; + case EXIT_REASON_DEVICE_FOLDED: + return SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; + case EXIT_REASON_DRAG_DIVIDER: + return SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; + case EXIT_REASON_RETURN_HOME: + return SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; + case EXIT_REASON_ROOT_TASK_VANISHED: + return SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED; + case EXIT_REASON_SCREEN_LOCKED: + return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED; + case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP: + return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; + case EXIT_REASON_UNKNOWN: + // Fall through + default: + Slog.e("SplitscreenEventLogger", "Unknown exit reason: " + exitReason); + return SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT; + } + } + + /** + * Logs when the user exits splitscreen. Only one of the main or side stages should be + * specified to indicate which position was focused as a part of exiting (both can be unset). + */ + public void logExit(@ExitReason int exitReason, + @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if ((mainStagePosition != SPLIT_POSITION_UNDEFINED + && sideStagePosition != SPLIT_POSITION_UNDEFINED) + || (mainStageUid != 0 && sideStageUid != 0)) { + throw new IllegalArgumentException("Only main or side stage should be set"); + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT, + 0 /* enterReason */, + getLoggerExitReason(exitReason), + 0f /* splitRatio */, + getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid, + getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + + // Reset states + mLoggerSessionId = null; + mDragEnterPosition = SPLIT_POSITION_UNDEFINED; + mDragEnterSessionId = null; + mLastMainStagePosition = -1; + mLastMainStageUid = -1; + mLastSideStagePosition = -1; + mLastSideStageUid = -1; + } + + /** + * Logs when an app in the main stage changes. + */ + public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, + isLandscape), mainStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + 0 /* sideStagePosition */, + 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when an app in the side stage changes. + */ + public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, + isLandscape), sideStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + 0 /* mainStagePosition */, + 0 /* mainStageUid */, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the splitscreen ratio changes. + */ + public void logResize(float splitRatio) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (splitRatio <= 0f || splitRatio >= 1f) { + // Don't bother reporting resizes that end up dismissing the split, that will be logged + // via the exit event + return; + } + if (!updateSplitRatioState(splitRatio)) { + // Ignore if there are no user perceived changes + return; + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE, + 0 /* enterReason */, + 0 /* exitReason */, + mLastSplitRatio, + 0 /* mainStagePosition */, 0 /* mainStageUid */, + 0 /* sideStagePosition */, 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the apps in splitscreen are swapped. + */ + public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + private boolean updateMainStageState(int mainStagePosition, int mainStageUid) { + boolean changed = (mLastMainStagePosition != mainStagePosition) + || (mLastMainStageUid != mainStageUid); + if (!changed) { + return false; + } + + mLastMainStagePosition = mainStagePosition; + mLastMainStageUid = mainStageUid; + return true; + } + + private boolean updateSideStageState(int sideStagePosition, int sideStageUid) { + boolean changed = (mLastSideStagePosition != sideStagePosition) + || (mLastSideStageUid != sideStageUid); + if (!changed) { + return false; + } + + mLastSideStagePosition = sideStagePosition; + mLastSideStageUid = sideStageUid; + return true; + } + + private boolean updateSplitRatioState(float splitRatio) { + boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0; + if (!changed) { + return false; + } + + mLastSplitRatio = splitRatio; + return true; + } + + public int getDragEnterReasonFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM; + } + } + + private int getMainStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM; + } + } + + private int getSideStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM; + } + } +} 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 0264c5a1c55a..5d1d159e63e6 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 @@ -17,24 +17,34 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.transitTypeToString; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_TOP_OR_LEFT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP; +import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString; import static com.android.wm.shell.splitscreen.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; import static com.android.wm.shell.transition.Transitions.isClosingType; import static com.android.wm.shell.transition.Transitions.isOpeningType; @@ -42,37 +52,59 @@ import static com.android.wm.shell.transition.Transitions.isOpeningType; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.app.WindowConfiguration; import android.content.Context; +import android.content.Intent; import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; import android.util.Log; +import android.util.Slog; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; import android.window.DisplayAreaInfo; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; -import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.InstanceId; import com.android.internal.protolog.common.ProtoLog; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; -import com.android.wm.shell.common.split.SplitLayout.SplitPosition; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.common.split.SplitWindowManager; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.recents.RecentTasksController; +import com.android.wm.shell.splitscreen.SplitScreen.StageType; +import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.util.StagedSplitBounds; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + +import javax.inject.Provider; /** * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and @@ -99,8 +131,10 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final MainStage mMainStage; private final StageListenerImpl mMainStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mMainUnfoldController; private final SideStage mSideStage; private final StageListenerImpl mSideStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mSideUnfoldController; @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; @@ -114,14 +148,24 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final Context mContext; private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; private final SplitScreenTransitions mSplitTransitions; - private boolean mExitSplitScreenOnHide = true; - - // TODO(b/187041611): remove this flag after totally deprecated legacy split - /** Whether the device is supporting legacy split or not. */ - private boolean mUseLegacySplit; - - @SplitScreen.StageType int mDismissTop = NO_DISMISS; + private final SplitscreenEventLogger mLogger; + private final Optional<RecentTasksController> mRecentTasks; + // Tracks whether we should update the recent tasks. Only allow this to happen in between enter + // and exit, since exit itself can trigger a number of changes that update the stages. + private boolean mShouldUpdateRecents; + private boolean mExitSplitScreenOnHide; + private boolean mKeyguardOccluded; + private boolean mDeviceSleep; + private boolean mIsDividerRemoteAnimating; + + @StageType + private int mDismissTop = NO_DISMISS; + + /** The target stage to dismiss to when unlock after folded. */ + @StageType + private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; private final Runnable mOnTransitionAnimationComplete = () -> { // If still playing, let it finish. @@ -134,29 +178,62 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mDismissTop = NO_DISMISS; }; + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = + new SplitWindowManager.ParentContainerCallbacks() { + @Override + public void attachToParentSurface(SurfaceControl.Builder b) { + mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b); + } + + @Override + public void onLeashReady(SurfaceControl leash) { + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + } + }; + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, - DisplayImeController displayImeController, Transitions transitions, - TransactionPool transactionPool) { + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, SplitscreenEventLogger logger, + IconProvider iconProvider, + Optional<RecentTasksController> recentTasks, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; mRootTDAOrganizer = rootTDAOrganizer; mTaskOrganizer = taskOrganizer; + mLogger = logger; + mRecentTasks = recentTasks; + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + mMainStage = new MainStage( + mContext, mTaskOrganizer, mDisplayId, mMainStageListener, mSyncQueue, - mSurfaceSession); + mSurfaceSession, + iconProvider, + mMainUnfoldController); mSideStage = new SideStage( + mContext, mTaskOrganizer, mDisplayId, mSideStageListener, mSyncQueue, - mSurfaceSession); + mSurfaceSession, + iconProvider, + mSideUnfoldController); mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mRootTDAOrganizer.registerListener(displayId, this); + final DeviceStateManager deviceStateManager = + mContext.getSystemService(DeviceStateManager.class); + deviceStateManager.registerCallback(taskOrganizer.getExecutor(), + new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged)); mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, mOnTransitionAnimationComplete); transitions.addHandler(this); @@ -166,7 +243,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController, - SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) { + DisplayInsetsController displayInsetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, + SplitscreenEventLogger logger, + Optional<RecentTasksController> recentTasks, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -175,10 +256,15 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage = mainStage; mSideStage = sideStage; mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; mRootTDAOrganizer.registerListener(displayId, this); mSplitLayout = splitLayout; mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, mOnTransitionAnimationComplete); + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + mLogger = logger; + mRecentTasks = recentTasks; transitions.addHandler(this); } @@ -191,12 +277,45 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mSideStageListener.mVisible && mMainStageListener.mVisible; } - boolean moveToSideStage(ActivityManager.RunningTaskInfo task, - @SplitPosition int sideStagePosition) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - setSideStagePosition(sideStagePosition); - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.addTask(task, getSideStageBounds(), wct); + @StageType + int getStageOfTask(int taskId) { + if (mMainStage.containsTask(taskId)) { + return STAGE_TYPE_MAIN; + } else if (mSideStage.containsTask(taskId)) { + return STAGE_TYPE_SIDE; + } + + return STAGE_TYPE_UNDEFINED; + } + + boolean moveToStage(ActivityManager.RunningTaskInfo task, @StageType int stageType, + @SplitPosition int stagePosition, WindowContainerTransaction wct) { + StageTaskListener targetStage; + int sideStagePosition; + if (stageType == STAGE_TYPE_MAIN) { + targetStage = mMainStage; + sideStagePosition = SplitLayout.reversePosition(stagePosition); + } else if (stageType == STAGE_TYPE_SIDE) { + targetStage = mSideStage; + sideStagePosition = stagePosition; + } else { + if (mMainStage.isActive()) { + // If the split screen is activated, retrieves target stage based on position. + targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage; + sideStagePosition = mSideStagePosition; + } else { + targetStage = mSideStage; + sideStagePosition = stagePosition; + } + } + + setSideStagePosition(sideStagePosition, wct); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + targetStage.evictAllChildren(evictWct); + targetStage.addTask(task, wct); + if (!evictWct.isEmpty()) { + wct.merge(evictWct, true /* transfer */); + } mTaskOrganizer.applyTransaction(wct); return true; } @@ -217,18 +336,19 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, /** Starts 2 tasks in one transition. */ void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, - @Nullable Bundle sideOptions, @SplitPosition int sidePosition, - @Nullable IRemoteTransition remoteTransition) { + @Nullable Bundle sideOptions, @SplitPosition int sidePosition, float splitRatio, + @Nullable RemoteTransition remoteTransition) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mainOptions = mainOptions != null ? mainOptions : new Bundle(); sideOptions = sideOptions != null ? sideOptions : new Bundle(); - setSideStagePosition(sidePosition); + setSideStagePosition(sidePosition, wct); // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - mMainStage.activate(getMainStageBounds(), wct); + mMainStage.activate(getMainStageBounds(), wct, false /* reparent */); mSideStage.setBounds(getSideStageBounds(), wct); + mSplitLayout.setDivideRatio(splitRatio); // Make sure the launch options will put tasks in the corresponding split roots addActivityOptions(mainOptions, mMainStage); addActivityOptions(sideOptions, mSideStage); @@ -241,29 +361,223 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this); } - @SplitLayout.SplitPosition + /** Starts 2 tasks in one legacy transition. */ + void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + float splitRatio, RemoteAnimationAdapter adapter) { + // Init divider first to make divider leash for remote animation target. + setDividerVisibility(true /* visible */); + // Set false to avoid record new bounds with old task still on top; + mShouldUpdateRecents = false; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + final WindowContainerTransaction evictWct = new WindowContainerTransaction(); + prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct); + prepareEvictChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, evictWct); + // Need to add another wrapper here in shell so that we can inject the divider bar + // and also manage the process elevation via setRunningRemote + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + mIsDividerRemoteAnimating = true; + RemoteAnimationTarget[] augmentedNonApps = + new RemoteAnimationTarget[nonApps.length + 1]; + for (int i = 0; i < nonApps.length; ++i) { + augmentedNonApps[i] = nonApps[i]; + } + augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget(); + + IRemoteAnimationFinishedCallback wrapCallback = + new IRemoteAnimationFinishedCallback.Stub() { + @Override + public void onAnimationFinished() throws RemoteException { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + mSyncQueue.queue(evictWct); + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + finishedCallback.onAnimationFinished(); + } + }; + try { + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( + adapter.getCallingApplication()); + } catch (SecurityException e) { + Slog.e(TAG, "Unable to boost animation thread. This should only happen" + + " during unit tests"); + } + adapter.getRunner().onAnimationStart(transit, apps, wallpapers, + augmentedNonApps, wrapCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + + @Override + public void onAnimationCancelled() { + mIsDividerRemoteAnimating = false; + mShouldUpdateRecents = true; + mSyncQueue.queue(evictWct); + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + try { + adapter.getRunner().onAnimationCancelled(); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + }; + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( + wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); + + if (mainOptions == null) { + mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); + } else { + ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); + mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + mainOptions = mainActivityOptions.toBundle(); + } + + sideOptions = sideOptions != null ? sideOptions : new Bundle(); + setSideStagePosition(sidePosition, wct); + + mSplitLayout.setDivideRatio(splitRatio); + if (mMainStage.isActive()) { + mMainStage.moveToTop(getMainStageBounds(), wct); + } else { + // Build a request WCT that will launch both apps such that task 0 is on the main stage + // while task 1 is on the side stage. + mMainStage.activate(getMainStageBounds(), wct, false /* reparent */); + } + mSideStage.moveToTop(getSideStageBounds(), wct); + + // Make sure the launch options will put tasks in the corresponding split roots + addActivityOptions(mainOptions, mMainStage); + addActivityOptions(sideOptions, mSideStage); + + // Add task launch requests + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + + // Using legacy transitions, so we can't use blast sync since it conflicts. + mTaskOrganizer.applyTransaction(wct); + } + + public void startIntent(PendingIntent intent, Intent fillInIntent, + @StageType int stage, @SplitPosition int position, + @androidx.annotation.Nullable Bundle options, + @Nullable RemoteTransition remoteTransition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + options = resolveStartStage(stage, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSplitTransitions.startEnterTransition( + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this); + } + + /** + * Collects all the current child tasks of a specific split and prepares transaction to evict + * them to display. + */ + void prepareEvictChildTasks(@SplitPosition int position, WindowContainerTransaction wct) { + if (position == mSideStagePosition) { + mSideStage.evictAllChildren(wct); + } else { + mMainStage.evictAllChildren(wct); + } + } + + Bundle resolveStartStage(@StageType int stage, + @SplitPosition int position, @androidx.annotation.Nullable Bundle options, + @androidx.annotation.Nullable WindowContainerTransaction wct) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: { + if (position != SPLIT_POSITION_UNDEFINED) { + if (mMainStage.isActive()) { + // Use the stage of the specified position + options = resolveStartStage( + position == mSideStagePosition ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN, + position, options, wct); + } else { + // Use the side stage as default to active split screen + options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct); + } + } else { + // Exit split-screen and launch fullscreen since stage wasn't specified. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + } + break; + } + case STAGE_TYPE_SIDE: { + if (position != SPLIT_POSITION_UNDEFINED) { + setSideStagePosition(position, wct); + } else { + position = getSideStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + case STAGE_TYPE_MAIN: { + if (position != SPLIT_POSITION_UNDEFINED) { + // Set the side stage opposite of what we want to the main stage. + setSideStagePosition(SplitLayout.reversePosition(position), wct); + } else { + position = getMainStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + default: + throw new IllegalArgumentException("Unknown stage=" + stage); + } + + return options; + } + + @SplitPosition int getSideStagePosition() { return mSideStagePosition; } - @SplitLayout.SplitPosition + @SplitPosition int getMainStagePosition() { - return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + return SplitLayout.reversePosition(mSideStagePosition); } - void setSideStagePosition(@SplitPosition int sideStagePosition) { - setSideStagePosition(sideStagePosition, true /* updateVisibility */); + int getTaskId(@SplitPosition int splitPosition) { + if (mSideStagePosition == splitPosition) { + return mSideStage.getTopVisibleChildTaskId(); + } else { + return mMainStage.getTopVisibleChildTaskId(); + } + } + + void setSideStagePosition(@SplitPosition int sideStagePosition, + @Nullable WindowContainerTransaction wct) { + setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); } - private void setSideStagePosition(@SplitPosition int sideStagePosition, - boolean updateVisibility) { + private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, + @Nullable WindowContainerTransaction wct) { if (mSideStagePosition == sideStagePosition) return; mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); - if (mSideStageListener.mVisible && updateVisibility) { - onStageVisibilityChanged(mSideStageListener); + if (mSideStageListener.mVisible && updateBounds) { + if (wct == null) { + // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. + onLayoutSizeChanged(mSplitLayout); + } else { + updateWindowBounds(mSplitLayout, wct); + updateUnfoldBounds(); + } } } @@ -275,24 +589,133 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } - void exitSplitScreen() { - exitSplitScreen(null /* childrenToTop */); + void onKeyguardOccludedChanged(boolean occluded) { + // Do not exit split directly, because it needs to wait for task info update to determine + // which task should remain on top after split dismissed. + mKeyguardOccluded = occluded; + } + + void onKeyguardVisibilityChanged(boolean showing) { + if (!showing && mMainStage.isActive() + && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { + exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, + EXIT_REASON_DEVICE_FOLDED); + } + } + + void onFinishedWakingUp() { + if (mMainStage.isActive()) { + exitSplitScreenIfKeyguardOccluded(); + } + mDeviceSleep = false; + } + + void onFinishedGoingToSleep() { + mDeviceSleep = true; } void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { mExitSplitScreenOnHide = exitSplitScreenOnHide; } - private void exitSplitScreen(StageTaskListener childrenToTop) { + void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) { + if (!mMainStage.isActive()) return; + + StageTaskListener childrenToTop = null; + if (mMainStage.containsTask(toTopTaskId)) { + childrenToTop = mMainStage; + } else if (mSideStage.containsTask(toTopTaskId)) { + childrenToTop = mSideStage; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (childrenToTop != null) { + childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct); + } + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void exitSplitScreen(StageTaskListener childrenToTop, @ExitReason int exitReason) { + if (!mMainStage.isActive()) return; + final WindowContainerTransaction wct = new WindowContainerTransaction(); - mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); - mMainStage.deactivate(wct, childrenToTop == mMainStage); + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void exitSplitScreenIfKeyguardOccluded() { + final boolean mainStageVisible = mMainStageListener.mVisible; + final boolean oneStageVisible = mainStageVisible ^ mSideStageListener.mVisible; + if (mDeviceSleep && mKeyguardOccluded && oneStageVisible) { + // Only the stages include show-when-locked activity is visible while keyguard occluded. + // Dismiss split because there's show-when-locked activity showing on top of keyguard. + // Also make sure the task contains show-when-locked activity remains on top after split + // dismissed. + final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage; + exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + } + } + + private void applyExitSplitScreen(StageTaskListener childrenToTop, + WindowContainerTransaction wct, @ExitReason int exitReason) { + mRecentTasks.ifPresent(recentTasks -> { + // Notify recents if we are exiting in a way that breaks the pair, and disable further + // updates to splits in the recents until we enter split again + if (shouldBreakPairedTaskInRecents(exitReason) && mShouldUpdateRecents) { + recentTasks.removeSplitPair(mMainStage.getTopVisibleChildTaskId()); + recentTasks.removeSplitPair(mSideStage.getTopVisibleChildTaskId()); + } + }); + mShouldUpdateRecents = false; + + // When the exit split-screen is caused by one of the task enters auto pip, + // we want the tasks to be put to bottom instead of top, otherwise it will end up + // a fullscreen plus a pinned task instead of pinned only at the end of the transition. + final boolean fromEnteringPip = exitReason == EXIT_REASON_CHILD_TASK_ENTER_PIP; + mSideStage.removeAllTasks(wct, !fromEnteringPip && childrenToTop == mSideStage); + mMainStage.deactivate(wct, !fromEnteringPip && childrenToTop == mMainStage); mTaskOrganizer.applyTransaction(wct); - // Reset divider position. + mSyncQueue.runInSync(t -> t + .setWindowCrop(mMainStage.mRootLeash, null) + .setWindowCrop(mSideStage.mRootLeash, null)); + + // Hide divider and reset its position. + setDividerVisibility(false); mSplitLayout.resetDividerPosition(); + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + Slog.i(TAG, "applyExitSplitScreen, reason = " + exitReasonToString(exitReason)); + // Log the exit + if (childrenToTop != null) { + logExitToStage(exitReason, childrenToTop == mMainStage); + } else { + logExit(exitReason); + } + } + + /** + * Returns whether the split pair in the recent tasks list should be broken. + */ + private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) { + switch (exitReason) { + // One of the apps doesn't support MW + case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW: + // User has explicitly dragged the divider to dismiss split + case EXIT_REASON_DRAG_DIVIDER: + // Either of the split apps have finished + case EXIT_REASON_APP_FINISHED: + // One of the children enters PiP + case EXIT_REASON_CHILD_TASK_ENTER_PIP: + return true; + default: + return false; + } } - private void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop, + /** + * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates + * an existing WindowContainerTransaction (rather than applying immediately). This is intended + * to be used when exiting split might be bundled with other window operations. + */ + void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct) { mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); @@ -303,35 +726,42 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, outBottomOrRightBounds.set(mSplitLayout.getBounds2()); } + @SplitPosition + int getSplitPosition(int taskId) { + if (mSideStage.getTopVisibleChildTaskId() == taskId) { + return getSideStagePosition(); + } else if (mMainStage.getTopVisibleChildTaskId() == taskId) { + return getMainStagePosition(); + } + return SPLIT_POSITION_UNDEFINED; + } + private void addActivityOptions(Bundle opts, StageTaskListener stage) { opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); } void updateActivityOptions(Bundle opts, @SplitPosition int position) { addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage); - - if (!mMainStage.isActive()) { - // Activate the main stage in anticipation of an app launch. - final WindowContainerTransaction wct = new WindowContainerTransaction(); - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.setBounds(getSideStageBounds(), wct); - mTaskOrganizer.applyTransaction(wct); - } } void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { if (mListeners.contains(listener)) return; mListeners.add(listener); - listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); - listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); - mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); - mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + sendStatusToListener(listener); } void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { mListeners.remove(listener); } + void sendStatusToListener(SplitScreen.SplitScreenListener listener) { + listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + listener.onSplitVisibilityChanged(isSplitScreenVisible()); + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } + private void sendOnStagePositionChanged() { for (int i = mListeners.size() - 1; i >= 0; --i) { final SplitScreen.SplitScreenListener l = mListeners.get(i); @@ -340,9 +770,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - private void onStageChildTaskStatusChanged( - StageListenerImpl stageListener, int taskId, boolean present, boolean visible) { - + private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, + boolean present, boolean visible) { int stage; if (present) { stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; @@ -350,25 +779,74 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // No longer on any stage stage = STAGE_TYPE_UNDEFINED; } + if (stage == STAGE_TYPE_MAIN) { + mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } else { + mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + if (present && visible) { + updateRecentTasksSplitPair(); + } for (int i = mListeners.size() - 1; i >= 0; --i) { mListeners.get(i).onTaskStageChanged(taskId, stage, visible); } } + private void onStageChildTaskEnterPip(StageListenerImpl stageListener, int taskId) { + exitSplitScreen(stageListener == mMainStageListener ? mMainStage : mSideStage, + EXIT_REASON_CHILD_TASK_ENTER_PIP); + } + + private void updateRecentTasksSplitPair() { + if (!mShouldUpdateRecents) { + return; + } + mRecentTasks.ifPresent(recentTasks -> { + Rect topLeftBounds = mSplitLayout.getBounds1(); + Rect bottomRightBounds = mSplitLayout.getBounds2(); + int mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId(); + int sideStageTopTaskId = mSideStage.getTopVisibleChildTaskId(); + boolean sideStageTopLeft = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; + int leftTopTaskId; + int rightBottomTaskId; + if (sideStageTopLeft) { + leftTopTaskId = sideStageTopTaskId; + rightBottomTaskId = mainStageTopTaskId; + } else { + leftTopTaskId = mainStageTopTaskId; + rightBottomTaskId = sideStageTopTaskId; + } + StagedSplitBounds splitBounds = new StagedSplitBounds(topLeftBounds, bottomRightBounds, + leftTopTaskId, rightBottomTaskId); + if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) { + // Update the pair for the top tasks + recentTasks.addSplitPair(mainStageTopTaskId, sideStageTopTaskId, splitBounds); + } + }); + } + + private void sendSplitVisibilityChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onSplitVisibilityChanged(mDividerVisible); + } + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); + mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); + } + } + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { - mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); final WindowContainerTransaction wct = new WindowContainerTransaction(); // Make the stages adjacent to each other so they occlude what's behind them. - wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); - - // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy - // split to prevent new split behavior confusing users. - if (!mUseLegacySplit) { - wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - } - + wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, + true /* moveTogether */); + wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); mTaskOrganizer.applyTransaction(wct); } } @@ -376,99 +854,77 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void onStageRootTaskVanished(StageListenerImpl stageListener) { if (stageListener == mMainStageListener || stageListener == mSideStageListener) { final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); // Deactivate the main stage if it no longer has a root task. mMainStage.deactivate(wct); - - if (!mUseLegacySplit) { - wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); - } - mTaskOrganizer.applyTransaction(wct); } } private void setDividerVisibility(boolean visible) { - if (mDividerVisible == visible) return; + if (mIsDividerRemoteAnimating || mDividerVisible == visible) return; mDividerVisible = visible; if (visible) { mSplitLayout.init(); + updateUnfoldBounds(); } else { mSplitLayout.release(); } + sendSplitVisibilityChanged(); } private void onStageVisibilityChanged(StageListenerImpl stageListener) { final boolean sideStageVisible = mSideStageListener.mVisible; final boolean mainStageVisible = mMainStageListener.mVisible; - // Divider is only visible if both the main stage and side stages are visible - setDividerVisibility(isSplitScreenVisible()); - - if (mExitSplitScreenOnHide && !mainStageVisible && !sideStageVisible) { - // Exit split-screen if both stage are not visible. - // TODO: This is only a temporary request from UX and is likely to be removed soon... - exitSplitScreen(); + final boolean bothStageVisible = sideStageVisible && mainStageVisible; + final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible; + final boolean sameVisibility = sideStageVisible == mainStageVisible; + // Only add or remove divider when both visible or both invisible to avoid sometimes we only + // got one stage visibility changed for a moment and it will cause flicker. + if (sameVisibility) { + setDividerVisibility(bothStageVisible); } - if (mainStageVisible) { - final WindowContainerTransaction wct = new WindowContainerTransaction(); - if (sideStageVisible) { - // The main stage configuration should to follow split layout when side stage is - // visible. - mMainStage.updateConfiguration( - WINDOWING_MODE_MULTI_WINDOW, getMainStageBounds(), wct); - } else { - // We want the main stage configuration to be fullscreen when the side stage isn't - // visible. - mMainStage.updateConfiguration(WINDOWING_MODE_FULLSCREEN, null, wct); + if (bothStageInvisible) { + if (mExitSplitScreenOnHide + // Don't dismiss staged split when both stages are not visible due to sleeping + // display, like the cases keyguard showing or screen off. + || (!mMainStage.mRootTaskInfo.isSleeping + && !mSideStage.mRootTaskInfo.isSleeping)) { + // Don't dismiss staged split when both stages are not visible due to sleeping display, + // like the cases keyguard showing or screen off. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); } - // TODO: Change to `mSyncQueue.queue(wct)` once BLAST is stable. - mTaskOrganizer.applyTransaction(wct); } + exitSplitScreenIfKeyguardOccluded(); mSyncQueue.runInSync(t -> { - final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); - final SurfaceControl sideStageLeash = mSideStage.mRootLeash; - final SurfaceControl mainStageLeash = mMainStage.mRootLeash; - - if (dividerLeash != null) { - if (mDividerVisible) { - t.show(dividerLeash) - .setLayer(dividerLeash, Integer.MAX_VALUE) - .setPosition(dividerLeash, - mSplitLayout.getDividerBounds().left, - mSplitLayout.getDividerBounds().top); - } else { - t.hide(dividerLeash); - } + // Same above, we only set root tasks and divider leash visibility when both stage + // change to visible or invisible to avoid flicker. + if (sameVisibility) { + t.setVisibility(mSideStage.mRootLeash, bothStageVisible) + .setVisibility(mMainStage.mRootLeash, bothStageVisible); + applyDividerVisibility(t); } + }); + } - if (sideStageVisible) { - final Rect sideStageBounds = getSideStageBounds(); - t.show(sideStageLeash) - .setPosition(sideStageLeash, - sideStageBounds.left, sideStageBounds.top) - .setWindowCrop(sideStageLeash, - sideStageBounds.width(), sideStageBounds.height()); - } else { - t.hide(sideStageLeash); - } + private void applyDividerVisibility(SurfaceControl.Transaction t) { + if (mIsDividerRemoteAnimating) return; - if (mainStageVisible) { - final Rect mainStageBounds = getMainStageBounds(); - t.show(mainStageLeash); - if (sideStageVisible) { - t.setPosition(mainStageLeash, mainStageBounds.left, mainStageBounds.top) - .setWindowCrop(mainStageLeash, - mainStageBounds.width(), mainStageBounds.height()); - } else { - // Clear window crop and position if side stage isn't visible. - t.setPosition(mainStageLeash, 0, 0) - .setWindowCrop(mainStageLeash, null); - } - } else { - t.hide(mainStageLeash); - } - }); + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) return; + + if (mDividerVisible) { + t.show(dividerLeash) + .setAlpha(dividerLeash, 1) + .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top); + } else { + t.hide(dividerLeash); + } } private void onStageHasChildrenChanged(StageListenerImpl stageListener) { @@ -477,21 +933,29 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!hasChildren) { if (isSideStage && mMainStageListener.mVisible) { // Exit to main stage if side stage no longer has children. - exitSplitScreen(mMainStage); + exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED); } else if (!isSideStage && mSideStageListener.mVisible) { // Exit to side stage if main stage no longer has children. - exitSplitScreen(mSideStage); + exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED); } } else if (isSideStage) { final WindowContainerTransaction wct = new WindowContainerTransaction(); // Make sure the main stage is active. - mMainStage.activate(getMainStageBounds(), wct); - mSideStage.setBounds(getSideStageBounds(), wct); - // Reorder side stage to the top whenever there's a new child task appeared in side - // stage. This is needed to prevent main stage occludes side stage and makes main stage - // flipping between fullscreen and multi-window windowing mode. - wct.reorder(mSideStage.mRootTaskInfo.token, true); - mTaskOrganizer.applyTransaction(wct); + mMainStage.activate(getMainStageBounds(), wct, true /* reparent */); + mSideStage.moveToTop(getSideStageBounds(), wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(mSplitLayout, t)); + } + if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) { + mShouldUpdateRecents = true; + updateRecentTasksSplitPair(); + + if (!mLogger.hasStartedSession()) { + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } } } @@ -511,38 +975,71 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, onSnappedToDismissTransition(mainStageToTop); return; } - exitSplitScreen(mainStageToTop ? mMainStage : mSideStage); + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, EXIT_REASON_DRAG_DIVIDER); } @Override public void onDoubleTappedDivider() { - setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT); + setSideStagePosition(SplitLayout.reversePosition(mSideStagePosition), null /* wct */); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + + @Override + public void onLayoutPositionChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + } + + @Override + public void onLayoutSizeChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(layout, t); + mMainStage.onResizing(getMainStageBounds(), t); + mSideStage.onResizing(getSideStageBounds(), t); + }); } @Override - public void onBoundsChanging(SplitLayout layout) { + public void onLayoutSizeChanged(SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateWindowBounds(layout, wct); + updateUnfoldBounds(); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> { + updateSurfaceBounds(layout, t); + mMainStage.onResized(getMainStageBounds(), t); + mSideStage.onResized(getSideStageBounds(), t); + }); + mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); + } + + private void updateUnfoldBounds() { + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onLayoutChanged(getMainStageBounds()); + mSideUnfoldController.onLayoutChanged(getSideStageBounds()); + } + } + + /** + * Populates `wct` with operations that match the split windows to the current layout. + * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied + */ + private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - - mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash, - bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer)); + layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); } - @Override - public void onBoundsChanged(SplitLayout layout) { + void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) { final StageTaskListener topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; final StageTaskListener bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - - final WindowContainerTransaction wct = new WindowContainerTransaction(); - layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); - mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> layout.applySurfaceChanges(t, topLeftStage.mRootLeash, - bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer)); + (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash, + bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer); } @Override @@ -561,13 +1058,30 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override + public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo, + bottomRightStage.mRootTaskInfo); + mTaskOrganizer.applyTransaction(wct); + } + + @Override public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { mDisplayAreaInfo = displayAreaInfo; if (mSplitLayout == null) { mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, - mDisplayAreaInfo.configuration, this, - b -> mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b), - mDisplayImeController, mTaskOrganizer); + mDisplayAreaInfo.configuration, this, mParentContainerCallbacks, + mDisplayImeController, mTaskOrganizer, false /* applyDismissingParallax */); + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.init(); + mSideUnfoldController.init(); + } } } @@ -580,8 +1094,20 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { mDisplayAreaInfo = displayAreaInfo; if (mSplitLayout != null - && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration)) { - onBoundsChanged(mSplitLayout); + && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration) + && mMainStage.isActive()) { + onLayoutSizeChanged(mSplitLayout); + } + } + + private void onFoldedStateChanged(boolean folded) { + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (!folded) return; + + if (mMainStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; } } @@ -612,7 +1138,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, return null; } - @SplitScreen.StageType + @StageType private int getStageType(StageTaskListener stage) { return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; } @@ -672,7 +1198,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (transition != mSplitTransitions.mPendingDismiss && transition != mSplitTransitions.mPendingEnter) { @@ -717,14 +1244,14 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean shouldAnimate = true; if (mSplitTransitions.mPendingEnter == transition) { - shouldAnimate = startPendingEnterAnimation(transition, info, t); + shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); } else if (mSplitTransitions.mPendingDismiss == transition) { - shouldAnimate = startPendingDismissAnimation(transition, info, t); + shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction); } if (!shouldAnimate) return false; - mSplitTransitions.playAnimation(transition, info, t, finishCallback, - mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, + finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); return true; } @@ -738,7 +1265,7 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, final TransitionInfo.Change change = info.getChanges().get(iC); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null || !taskInfo.hasParentTask()) continue; - final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo)); + final @StageType int stageType = getStageType(getStageOfTask(taskInfo)); if (stageType == STAGE_TYPE_MAIN) { mainChild = change; } else if (stageType == STAGE_TYPE_SIDE) { @@ -754,7 +1281,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Update local states (before animating). setDividerVisibility(true); - setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateVisibility */); + setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */, + null /* wct */); setSplitsVisible(true); addDividerBarToTransition(info, t, true /* show */); @@ -854,12 +1382,22 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. if (show) { t.setAlpha(leash, 1.f); - t.setLayer(leash, Integer.MAX_VALUE); + t.setLayer(leash, SPLIT_DIVIDER_LAYER); t.setPosition(leash, bounds.left, bounds.top); t.show(leash); } } + RemoteAnimationTarget getDividerBarLegacyTarget() { + final Rect bounds = mSplitLayout.getDividerBounds(); + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + @Override public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; @@ -867,11 +1405,16 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, pw.println(prefix + TAG + " mDisplayId=" + mDisplayId); pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); pw.println(innerPrefix + "MainStage"); + pw.println(childPrefix + "stagePosition=" + getMainStagePosition()); pw.println(childPrefix + "isActive=" + mMainStage.isActive()); mMainStageListener.dump(pw, childPrefix); pw.println(innerPrefix + "SideStage"); + pw.println(childPrefix + "stagePosition=" + getSideStagePosition()); mSideStageListener.dump(pw, childPrefix); - pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout); + if (mMainStage.isActive()) { + pw.println(innerPrefix + "SplitLayout"); + mSplitLayout.dump(pw, childPrefix); + } } /** @@ -884,6 +1427,36 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; } + /** + * Sets drag info to be logged when splitscreen is next entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mLogger.enterRequestedByDrag(position, dragSessionId); + } + + /** + * Logs the exit of splitscreen. + */ + private void logExit(@ExitReason int exitReason) { + mLogger.logExit(exitReason, + SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, + SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + + /** + * Logs the exit of splitscreen to a specific stage. This must be called before the exit is + * executed. + */ + private void logExitToStage(@ExitReason int exitReason, boolean toMainStage) { + mLogger.logExit(exitReason, + toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED, + toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, + !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, + !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { boolean mHasRootTask = false; boolean mVisible = false; @@ -915,6 +1488,11 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override + public void onChildTaskEnterPip(int taskId) { + StageCoordinator.this.onStageChildTaskEnterPip(this, taskId); + } + + @Override public void onRootTaskVanished() { reset(); StageCoordinator.this.onStageRootTaskVanished(this); @@ -923,7 +1501,8 @@ class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void onNoLongerSupportMultiWindow() { if (mMainStage.isActive()) { - StageCoordinator.this.exitSplitScreen(); + StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, + EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 0fd8eca6290e..04e20db369ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -16,27 +16,36 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.annotation.CallSuper; +import android.annotation.Nullable; import android.app.ActivityManager; +import android.content.Context; import android.graphics.Point; import android.graphics.Rect; import android.util.SparseArray; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; +import com.android.internal.R; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SurfaceUtils; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.split.SplitDecorManager; +import com.android.wm.shell.splitscreen.SplitScreen.StageType; import java.io.PrintWriter; @@ -66,27 +75,45 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); + void onChildTaskEnterPip(int taskId); + void onRootTaskVanished(); + void onNoLongerSupportMultiWindow(); } + private final Context mContext; private final StageListenerCallbacks mCallbacks; - private final SyncTransactionQueue mSyncQueue; private final SurfaceSession mSurfaceSession; + private final SyncTransactionQueue mSyncQueue; + private final IconProvider mIconProvider; protected ActivityManager.RunningTaskInfo mRootTaskInfo; protected SurfaceControl mRootLeash; protected SurfaceControl mDimLayer; protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>(); private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>(); + // TODO(b/204308910): Extracts SplitDecorManager related code to common package. + private SplitDecorManager mSplitDecorManager; - StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId, + private final StageTaskUnfoldController mStageTaskUnfoldController; + + StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, - SurfaceSession surfaceSession) { + SurfaceSession surfaceSession, IconProvider iconProvider, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + mContext = context; mCallbacks = callbacks; mSyncQueue = syncQueue; mSurfaceSession = surfaceSession; - taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + mIconProvider = iconProvider; + mStageTaskUnfoldController = stageTaskUnfoldController; + + // No need to create root task if the device is using legacy split screen. + // TODO(b/199236198): Remove this check after totally deprecated legacy split. + if (!context.getResources().getBoolean(R.bool.config_useLegacySplit)) { + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + } } int getChildCount() { @@ -97,12 +124,62 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { return mChildrenTaskInfo.contains(taskId); } + /** + * Returns the top visible child task's id. + */ + int getTopVisibleChildTaskId() { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); + if (info.isVisible) { + return info.taskId; + } + } + return INVALID_TASK_ID; + } + + /** + * Returns the top activity uid for the top child task. + */ + int getTopChildTaskUid() { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); + if (info.topActivityInfo == null) { + continue; + } + return info.topActivityInfo.applicationInfo.uid; + } + return 0; + } + + /** @return {@code true} if this listener contains the currently focused task. */ + boolean isFocused() { + if (mRootTaskInfo == null) { + return false; + } + + if (mRootTaskInfo.isFocused) { + return true; + } + + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + if (mChildrenTaskInfo.valueAt(i).isFocused) { + return true; + } + } + + return false; + } + @Override @CallSuper public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { if (mRootTaskInfo == null && !taskInfo.hasParentTask()) { mRootLeash = leash; mRootTaskInfo = taskInfo; + mSplitDecorManager = new SplitDecorManager( + mRootTaskInfo.configuration, + mIconProvider, + mSurfaceSession); mCallbacks.onRootTaskAppeared(); sendStatusChanged(); mSyncQueue.runInSync(t -> mDimLayer = @@ -122,6 +199,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash); + } } @Override @@ -133,6 +214,17 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { return; } if (mRootTaskInfo.taskId == taskInfo.taskId) { + // Inflates split decor view only when the root task is visible. + if (mRootTaskInfo.isVisible != taskInfo.isVisible) { + mSyncQueue.runInSync(t -> { + if (taskInfo.isVisible) { + mSplitDecorManager.inflate(mContext, mRootLeash, + taskInfo.configuration.windowConfiguration.getBounds()); + } else { + mSplitDecorManager.release(t); + } + }); + } mRootTaskInfo = taskInfo; } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); @@ -159,12 +251,18 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { final int taskId = taskInfo.taskId; if (mRootTaskInfo.taskId == taskId) { mCallbacks.onRootTaskVanished(); - mSyncQueue.runInSync(t -> t.remove(mDimLayer)); mRootTaskInfo = null; + mSyncQueue.runInSync(t -> { + t.remove(mDimLayer); + mSplitDecorManager.release(t); + }); } else if (mChildrenTaskInfo.contains(taskId)) { mChildrenTaskInfo.remove(taskId); mChildrenLeashes.remove(taskId); mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { + mCallbacks.onChildTaskEnterPip(taskId); + } if (ENABLE_SHELL_TRANSITIONS) { // Status is managed/synchronized by the transition lifecycle. return; @@ -174,6 +272,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskVanished(taskInfo); + } } @Override @@ -187,16 +289,52 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener { } } + void onResizing(Rect newBounds, SurfaceControl.Transaction t) { + if (mSplitDecorManager != null && mRootTaskInfo != null) { + mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, t); + } + } + + void onResized(Rect newBounds, SurfaceControl.Transaction t) { + if (mSplitDecorManager != null) { + mSplitDecorManager.onResized(newBounds, t); + } + } + + void addTask(ActivityManager.RunningTaskInfo task, WindowContainerTransaction wct) { + wct.reparent(task.token, mRootTaskInfo.token, true /* onTop*/); + } + + void moveToTop(Rect rootBounds, WindowContainerTransaction wct) { + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setBounds(rootToken, rootBounds).reorder(rootToken, true /* onTop */); + } + void setBounds(Rect bounds, WindowContainerTransaction wct) { wct.setBounds(mRootTaskInfo.token, bounds); } + void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { + if (!containsTask(taskId)) { + return; + } + wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */); + } + + /** Collects all the current child tasks and prepares transaction to evict them to display. */ + void evictAllChildren(WindowContainerTransaction wct) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) { + final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i); + wct.reparent(taskInfo.token, null /* parent */, false /* onTop */); + } + } + void setVisibility(boolean visible, WindowContainerTransaction wct) { wct.reorder(mRootTaskInfo.token, visible /* onTop */); } void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, - @SplitScreen.StageType int stage) { + @StageType int stage) { for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { int taskId = mChildrenTaskInfo.keyAt(i); listener.onTaskStageChanged(taskId, stage, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java new file mode 100644 index 000000000000..4849163e96fd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskUnfoldController.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2021 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.splitscreen; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.concurrent.Executor; + +/** + * Controls transformations of the split screen task surfaces in response + * to the unfolding/folding action on foldable devices + */ +public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener { + + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; + + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final ShellUnfoldProgressProvider mUnfoldProgressProvider; + private final DisplayInsetsController mDisplayInsetsController; + private final UnfoldBackgroundController mBackgroundController; + private final Executor mExecutor; + private final int mExpandedTaskBarHeight; + private final float mWindowCornerRadiusPx; + private final Rect mStageBounds = new Rect(); + private final TransactionPool mTransactionPool; + + private InsetsSource mTaskbarInsetsSource; + private boolean mBothStagesVisible; + + public StageTaskUnfoldController(@NonNull Context context, + @NonNull TransactionPool transactionPool, + @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, + @NonNull DisplayInsetsController displayInsetsController, + @NonNull UnfoldBackgroundController backgroundController, + @NonNull Executor executor) { + mUnfoldProgressProvider = unfoldProgressProvider; + mTransactionPool = transactionPool; + mExecutor = executor; + mBackgroundController = backgroundController; + mDisplayInsetsController = displayInsetsController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + /** + * Initializes the controller, starts listening for the external events + */ + public void init() { + mUnfoldProgressProvider.addListener(mExecutor, this); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + /** + * Called when split screen task appeared + * @param taskInfo info for the appeared task + * @param leash surface leash for the appeared task + */ + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + // Only handle child task surface here. + if (!taskInfo.hasParentTask()) return; + + AnimationContext context = new AnimationContext(leash); + mAnimationContextByTaskId.put(taskInfo.taskId, context); + } + + /** + * Called when a split screen task vanished + * @param taskInfo info for the vanished task + */ + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + if (!taskInfo.hasParentTask()) return; + + AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); + if (context != null) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + resetSurface(transaction, context); + transaction.apply(); + mTransactionPool.release(transaction); + } + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + @Override + public void onStateChangeProgress(float progress) { + if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return; + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + mBackgroundController.ensureBackground(transaction); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + + context.mCurrentCropRect.set(RECT_EVALUATOR + .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); + + transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + + transaction.apply(); + + mTransactionPool.release(transaction); + } + + @Override + public void onStateChangeFinished() { + resetTransformations(); + } + + /** + * Called when split screen visibility changes + * @param bothStagesVisible true if both stages of the split screen are visible + */ + public void onSplitVisibilityChanged(boolean bothStagesVisible) { + mBothStagesVisible = bothStagesVisible; + if (!bothStagesVisible) { + resetTransformations(); + } + } + + /** + * Called when split screen stage bounds changed + * @param bounds new bounds for this stage + */ + public void onLayoutChanged(Rect bounds) { + mStageBounds.set(bounds); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + private void resetTransformations() { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(transaction, context); + } + mBackgroundController.removeBackground(transaction); + transaction.apply(); + + mTransactionPool.release(transaction); + } + + private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) { + transaction + .setWindowCrop(context.mLeash, null) + .setCornerRadius(context.mLeash, 0.0F); + } + + private class AnimationContext { + final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); + final Rect mEndCropRect = new Rect(); + final Rect mCurrentCropRect = new Rect(); + + private AnimationContext(SurfaceControl leash) { + this.mLeash = leash; + update(); + } + + private void update() { + mStartCropRect.set(mStageBounds); + + if (mTaskbarInsetsSource != null) { + // Only insets the cropping window with taskbar when taskbar is expanded + if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mStartCropRect.inset(mTaskbarInsetsSource + .calculateVisibleInsets(mStartCropRect)); + } + } + + // Offset to surface coordinates as layout bounds are in screen coordinates + mStartCropRect.offsetTo(0, 0); + + mEndCropRect.set(mStartCropRect); + + int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height()); + int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION); + mStartCropRect.inset(margin, margin, margin, margin); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl new file mode 100644 index 000000000000..45f6d3c8b154 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreen.aidl @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.window.RemoteTransition; + +import com.android.wm.shell.stagesplit.ISplitScreenListener; + +/** + * Interface that is exposed to remote callers to manipulate the splitscreen feature. + */ +interface ISplitScreen { + + /** + * Registers a split screen listener. + */ + oneway void registerSplitScreenListener(in ISplitScreenListener listener) = 1; + + /** + * Unregisters a split screen listener. + */ + oneway void unregisterSplitScreenListener(in ISplitScreenListener listener) = 2; + + /** + * Hides the side-stage if it is currently visible. + */ + oneway void setSideStageVisibility(boolean visible) = 3; + + /** + * Removes a task from the side stage. + */ + oneway void removeFromSideStage(int taskId) = 4; + + /** + * Removes the split-screen stages and leaving indicated task to top. Passing INVALID_TASK_ID + * to indicate leaving no top task after leaving split-screen. + */ + oneway void exitSplitScreen(int toTopTaskId) = 5; + + /** + * @param exitSplitScreenOnHide if to exit split-screen if both stages are not visible. + */ + oneway void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) = 6; + + /** + * Starts a task in a stage. + */ + oneway void startTask(int taskId, int stage, int position, in Bundle options) = 7; + + /** + * Starts a shortcut in a stage. + */ + oneway void startShortcut(String packageName, String shortcutId, int stage, int position, + in Bundle options, in UserHandle user) = 8; + + /** + * Starts an activity in a stage. + */ + oneway void startIntent(in PendingIntent intent, in Intent fillInIntent, int stage, + int position, in Bundle options) = 9; + + /** + * Starts tasks simultaneously in one transition. + */ + oneway void startTasks(int mainTaskId, in Bundle mainOptions, int sideTaskId, + in Bundle sideOptions, int sidePosition, in RemoteTransition remoteTransition) = 10; + + /** + * Version of startTasks using legacy transition system. + */ + oneway void startTasksWithLegacyTransition(int mainTaskId, in Bundle mainOptions, + int sideTaskId, in Bundle sideOptions, int sidePosition, + in RemoteAnimationAdapter adapter) = 11; + + /** + * Blocking call that notifies and gets additional split-screen targets when entering + * recents (for example: the dividerBar). + * @param cancel is true if leaving recents back to split (eg. the gesture was cancelled). + * @param appTargets apps that will be re-parented to display area + */ + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + in RemoteAnimationTarget[] appTargets) = 12; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl new file mode 100644 index 000000000000..46e4299f99fa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/ISplitScreenListener.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +/** + * Listener interface that Launcher attaches to SystemUI to get split-screen callbacks. + */ +oneway interface ISplitScreenListener { + + /** + * Called when the stage position changes. + */ + void onStagePositionChanged(int stage, int position); + + /** + * Called when a task changes stages. + */ + void onTaskStageChanged(int taskId, int stage, boolean visible); +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java new file mode 100644 index 000000000000..83855be91e04 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/MainStage.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 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.stagesplit; + +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import android.annotation.Nullable; +import android.graphics.Rect; +import android.view.SurfaceSession; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Main stage for split-screen mode. When split-screen is active all standard activity types launch + * on the main stage, except for task that are explicitly pinned to the {@link SideStage}. + * @see StageCoordinator + */ +class MainStage extends StageTaskListener { + private static final String TAG = MainStage.class.getSimpleName(); + + private boolean mIsActive = false; + + MainStage(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + stageTaskUnfoldController); + } + + boolean isActive() { + return mIsActive; + } + + void activate(Rect rootBounds, WindowContainerTransaction wct) { + if (mIsActive) return; + + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setBounds(rootToken, rootBounds) + .setWindowingMode(rootToken, WINDOWING_MODE_MULTI_WINDOW) + .setLaunchRoot( + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES) + .reparentTasks( + null /* currentParent */, + rootToken, + CONTROLLED_WINDOWING_MODES, + CONTROLLED_ACTIVITY_TYPES, + true /* onTop */) + // Moving the root task to top after the child tasks were re-parented , or the root + // task cannot be visible and focused. + .reorder(rootToken, true /* onTop */); + + mIsActive = true; + } + + void deactivate(WindowContainerTransaction wct) { + deactivate(wct, false /* toTop */); + } + + void deactivate(WindowContainerTransaction wct, boolean toTop) { + if (!mIsActive) return; + mIsActive = false; + + if (mRootTaskInfo == null) return; + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setLaunchRoot( + rootToken, + null, + null) + .reparentTasks( + rootToken, + null /* newParent */, + CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + CONTROLLED_ACTIVITY_TYPES, + toTop) + // We want this re-order to the bottom regardless since we are re-parenting + // all its tasks. + .reorder(rootToken, false /* onTop */); + } + + void updateConfiguration(int windowingMode, Rect bounds, WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, bounds) + .setWindowingMode(mRootTaskInfo.token, windowingMode); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS new file mode 100644 index 000000000000..264e88f32bff --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OWNERS @@ -0,0 +1,2 @@ +# WM shell sub-modules stagesplit owner +chenghsiuchang@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java new file mode 100644 index 000000000000..8fbad52c630f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineManager.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.os.Binder; +import android.view.IWindow; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.LayoutInflater; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; + +import com.android.wm.shell.R; + +/** + * Handles drawing outline of the bounds of provided root surface. The outline will be drown with + * the consideration of display insets like status bar, navigation bar and display cutout. + */ +class OutlineManager extends WindowlessWindowManager { + private static final String WINDOW_NAME = "SplitOutlineLayer"; + private final Context mContext; + private final Rect mRootBounds = new Rect(); + private final Rect mTempRect = new Rect(); + private final Rect mLastOutlineBounds = new Rect(); + private final InsetsState mInsetsState = new InsetsState(); + private final int mExpandedTaskBarHeight; + private OutlineView mOutlineView; + private SurfaceControlViewHost mViewHost; + private SurfaceControl mHostLeash; + private SurfaceControl mLeash; + + OutlineManager(Context context, Configuration configuration) { + super(configuration, null /* rootSurface */, null /* hostInputToken */); + mContext = context.createWindowContext(context.getDisplay(), TYPE_APPLICATION_OVERLAY, + null /* options */); + mExpandedTaskBarHeight = mContext.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + @Override + protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) { + b.setParent(mHostLeash); + } + + void inflate(SurfaceControl rootLeash, Rect rootBounds) { + if (mLeash != null || mViewHost != null) return; + + mHostLeash = rootLeash; + mRootBounds.set(rootBounds); + mViewHost = new SurfaceControlViewHost(mContext, mContext.getDisplay(), this); + + final FrameLayout rootLayout = (FrameLayout) LayoutInflater.from(mContext) + .inflate(R.layout.split_outline, null); + mOutlineView = rootLayout.findViewById(R.id.split_outline); + + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + 0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); + lp.width = mRootBounds.width(); + lp.height = mRootBounds.height(); + lp.token = new Binder(); + lp.setTitle(WINDOW_NAME); + lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY; + // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports + // TRUSTED_OVERLAY for windowless window without input channel. + mViewHost.setView(rootLayout, lp); + mLeash = getSurfaceControl(mViewHost.getWindowToken()); + + drawOutline(); + } + + void release() { + if (mViewHost != null) { + mViewHost.release(); + mViewHost = null; + } + mRootBounds.setEmpty(); + mLastOutlineBounds.setEmpty(); + mOutlineView = null; + mHostLeash = null; + mLeash = null; + } + + @Nullable + SurfaceControl getOutlineLeash() { + return mLeash; + } + + void setVisibility(boolean visible) { + if (mOutlineView != null) { + mOutlineView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); + } + } + + void setRootBounds(Rect rootBounds) { + if (mViewHost == null || mViewHost.getView() == null) { + return; + } + + if (!mRootBounds.equals(rootBounds)) { + WindowManager.LayoutParams lp = + (WindowManager.LayoutParams) mViewHost.getView().getLayoutParams(); + lp.width = rootBounds.width(); + lp.height = rootBounds.height(); + mViewHost.relayout(lp); + mRootBounds.set(rootBounds); + drawOutline(); + } + } + + void onInsetsChanged(InsetsState insetsState) { + if (!mInsetsState.equals(insetsState)) { + mInsetsState.set(insetsState); + drawOutline(); + } + } + + private void computeOutlineBounds(Rect rootBounds, InsetsState insetsState, Rect outBounds) { + outBounds.set(rootBounds); + final InsetsSource taskBarInsetsSource = + insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + // Only insets the divider bar with task bar when it's expanded so that the rounded corners + // will be drawn against task bar. + if (taskBarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + outBounds.inset(taskBarInsetsSource.calculateVisibleInsets(outBounds)); + } + + // Offset the coordinate from screen based to surface based. + outBounds.offset(-rootBounds.left, -rootBounds.top); + } + + void drawOutline() { + if (mOutlineView == null) { + return; + } + + computeOutlineBounds(mRootBounds, mInsetsState, mTempRect); + if (mTempRect.equals(mLastOutlineBounds)) { + return; + } + + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) mOutlineView.getLayoutParams(); + lp.leftMargin = mTempRect.left; + lp.topMargin = mTempRect.top; + lp.width = mTempRect.width(); + lp.height = mTempRect.height(); + mOutlineView.setLayoutParams(lp); + mLastOutlineBounds.set(mTempRect); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java new file mode 100644 index 000000000000..92b1381fc808 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/OutlineView.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; +import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; +import static android.view.RoundedCorner.POSITION_TOP_LEFT; +import static android.view.RoundedCorner.POSITION_TOP_RIGHT; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.RoundedCorner; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.R; + +/** View for drawing split outline. */ +public class OutlineView extends View { + private final Paint mPaint = new Paint(); + private final Path mPath = new Path(); + private final float[] mRadii = new float[8]; + + public OutlineView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth( + getResources().getDimension(R.dimen.accessibility_focus_highlight_stroke_width)); + mPaint.setColor(getResources().getColor(R.color.system_accent1_100, null)); + } + + @Override + protected void onAttachedToWindow() { + // TODO(b/200850654): match the screen corners with the actual display decor. + mRadii[0] = mRadii[1] = getCornerRadius(POSITION_TOP_LEFT); + mRadii[2] = mRadii[3] = getCornerRadius(POSITION_TOP_RIGHT); + mRadii[4] = mRadii[5] = getCornerRadius(POSITION_BOTTOM_RIGHT); + mRadii[6] = mRadii[7] = getCornerRadius(POSITION_BOTTOM_LEFT); + } + + private int getCornerRadius(@RoundedCorner.Position int position) { + final RoundedCorner roundedCorner = getDisplay().getRoundedCorner(position); + return roundedCorner == null ? 0 : roundedCorner.getRadius(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (changed) { + mPath.reset(); + mPath.addRoundRect(0, 0, getWidth(), getHeight(), mRadii, Path.Direction.CW); + } + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawPath(mPath, mPaint); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java new file mode 100644 index 000000000000..55c4f3aea19a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SideStage.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2020 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.stagesplit; + +import android.annotation.CallSuper; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.SyncTransactionQueue; + +/** + * Side stage for split-screen mode. Only tasks that are explicitly pinned to this stage show up + * here. All other task are launch in the {@link MainStage}. + * + * @see StageCoordinator + */ +class SideStage extends StageTaskListener implements + DisplayInsetsController.OnInsetsChangedListener { + private static final String TAG = SideStage.class.getSimpleName(); + private final Context mContext; + private OutlineManager mOutlineManager; + + SideStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + super(taskOrganizer, displayId, callbacks, syncQueue, surfaceSession, + stageTaskUnfoldController); + mContext = context; + } + + void addTask(ActivityManager.RunningTaskInfo task, Rect rootBounds, + WindowContainerTransaction wct) { + final WindowContainerToken rootToken = mRootTaskInfo.token; + wct.setBounds(rootToken, rootBounds) + .reparent(task.token, rootToken, true /* onTop*/) + // Moving the root task to top after the child tasks were reparented , or the root + // task cannot be visible and focused. + .reorder(rootToken, true /* onTop */); + } + + boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { + // No matter if the root task is empty or not, moving the root to bottom because it no + // longer preserves visible child task. + wct.reorder(mRootTaskInfo.token, false /* onTop */); + if (mChildrenTaskInfo.size() == 0) return false; + wct.reparentTasks( + mRootTaskInfo.token, + null /* newParent */, + CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE, + CONTROLLED_ACTIVITY_TYPES, + toTop); + return true; + } + + boolean removeTask(int taskId, WindowContainerToken newParent, WindowContainerTransaction wct) { + final ActivityManager.RunningTaskInfo task = mChildrenTaskInfo.get(taskId); + if (task == null) return false; + wct.reparent(task.token, newParent, false /* onTop */); + return true; + } + + @Nullable + public SurfaceControl getOutlineLeash() { + return mOutlineManager.getOutlineLeash(); + } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + super.onTaskAppeared(taskInfo, leash); + if (isRootTask(taskInfo)) { + mOutlineManager = new OutlineManager(mContext, taskInfo.configuration); + enableOutline(true); + } + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + super.onTaskInfoChanged(taskInfo); + if (isRootTask(taskInfo)) { + mOutlineManager.setRootBounds(taskInfo.configuration.windowConfiguration.getBounds()); + } + } + + private boolean isRootTask(ActivityManager.RunningTaskInfo taskInfo) { + return mRootTaskInfo != null && mRootTaskInfo.taskId == taskInfo.taskId; + } + + void enableOutline(boolean enable) { + if (mOutlineManager == null) { + return; + } + + if (enable) { + if (mRootTaskInfo != null) { + mOutlineManager.inflate(mRootLeash, + mRootTaskInfo.configuration.windowConfiguration.getBounds()); + } + } else { + mOutlineManager.release(); + } + } + + void setOutlineVisibility(boolean visible) { + mOutlineManager.setVisibility(visible); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mOutlineManager.onInsetsChanged(insetsState); + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsChanged(insetsState); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java new file mode 100644 index 000000000000..c5d231262cd2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreen.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2020 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.stagesplit; + +import android.annotation.IntDef; +import android.annotation.NonNull; + +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; + +import java.util.concurrent.Executor; + +/** + * Interface to engage split-screen feature. + * TODO: Figure out which of these are actually needed outside of the Shell + */ +@ExternalThread +public interface SplitScreen { + /** + * Stage type isn't specified normally meaning to use what ever the default is. + * E.g. exit split-screen and launch the app in fullscreen. + */ + int STAGE_TYPE_UNDEFINED = -1; + /** + * The main stage type. + * @see MainStage + */ + int STAGE_TYPE_MAIN = 0; + + /** + * The side stage type. + * @see SideStage + */ + int STAGE_TYPE_SIDE = 1; + + @IntDef(prefix = { "STAGE_TYPE_" }, value = { + STAGE_TYPE_UNDEFINED, + STAGE_TYPE_MAIN, + STAGE_TYPE_SIDE + }) + @interface StageType {} + + /** Callback interface for listening to changes in a split-screen stage. */ + interface SplitScreenListener { + default void onStagePositionChanged(@StageType int stage, @SplitPosition int position) {} + default void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {} + default void onSplitVisibilityChanged(boolean visible) {} + } + + /** Registers listener that gets split screen callback. */ + void registerSplitScreenListener(@NonNull SplitScreenListener listener, + @NonNull Executor executor); + + /** Unregisters listener that gets split screen callback. */ + void unregisterSplitScreenListener(@NonNull SplitScreenListener listener); + + /** + * Returns a binder that can be passed to an external process to manipulate SplitScreen. + */ + default ISplitScreen createExternalInterface() { + return null; + } + + /** + * Called when the keyguard occluded state changes. + * @param occluded Indicates if the keyguard is now occluded. + */ + void onKeyguardOccludedChanged(boolean occluded); + + /** + * Called when the visibility of the keyguard changes. + * @param showing Indicates if the keyguard is now visible. + */ + void onKeyguardVisibilityChanged(boolean showing); + + /** Get a string representation of a stage type */ + static String stageTypeToString(@StageType int stage) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: return "UNDEFINED"; + case STAGE_TYPE_MAIN: return "MAIN"; + case STAGE_TYPE_SIDE: return "SIDE"; + default: return "UNKNOWN(" + stage + ")"; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java new file mode 100644 index 000000000000..f1520edf53b1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenController.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2020 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.stagesplit; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.RemoteAnimationTarget.MODE_OPENING; + +import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; + +import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; +import android.window.RemoteTransition; +import android.window.WindowContainerTransaction; + +import androidx.annotation.BinderThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.logging.InstanceId; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.RemoteCallable; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.draganddrop.DragAndDropPolicy; +import com.android.wm.shell.transition.LegacyTransitions; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.Executor; + +import javax.inject.Provider; + +/** + * Class manages split-screen multitasking mode and implements the main interface + * {@link SplitScreen}. + * @see StageCoordinator + */ +// TODO(b/198577848): Implement split screen flicker test to consolidate CUJ of split screen. +public class SplitScreenController implements DragAndDropPolicy.Starter, + RemoteCallable<SplitScreenController> { + private static final String TAG = SplitScreenController.class.getSimpleName(); + + private final ShellTaskOrganizer mTaskOrganizer; + private final SyncTransactionQueue mSyncQueue; + private final Context mContext; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private final ShellExecutor mMainExecutor; + private final SplitScreenImpl mImpl = new SplitScreenImpl(); + private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; + private final Transitions mTransitions; + private final TransactionPool mTransactionPool; + private final SplitscreenEventLogger mLogger; + private final Provider<Optional<StageTaskUnfoldController>> mUnfoldControllerProvider; + + private StageCoordinator mStageCoordinator; + + public SplitScreenController(ShellTaskOrganizer shellTaskOrganizer, + SyncTransactionQueue syncQueue, Context context, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, + ShellExecutor mainExecutor, DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, + Transitions transitions, TransactionPool transactionPool, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + mTaskOrganizer = shellTaskOrganizer; + mSyncQueue = syncQueue; + mContext = context; + mRootTDAOrganizer = rootTDAOrganizer; + mMainExecutor = mainExecutor; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mTransitions = transitions; + mTransactionPool = transactionPool; + mUnfoldControllerProvider = unfoldControllerProvider; + mLogger = new SplitscreenEventLogger(); + } + + public SplitScreen asSplitScreen() { + return mImpl; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public ShellExecutor getRemoteCallExecutor() { + return mMainExecutor; + } + + public void onOrganizerRegistered() { + if (mStageCoordinator == null) { + // TODO: Multi-display + mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, + mRootTDAOrganizer, mTaskOrganizer, mDisplayImeController, + mDisplayInsetsController, mTransitions, mTransactionPool, mLogger, + mUnfoldControllerProvider); + } + } + + public boolean isSplitScreenVisible() { + return mStageCoordinator.isSplitScreenVisible(); + } + + public boolean moveToSideStage(int taskId, @SplitPosition int sideStagePosition) { + final ActivityManager.RunningTaskInfo task = mTaskOrganizer.getRunningTaskInfo(taskId); + if (task == null) { + throw new IllegalArgumentException("Unknown taskId" + taskId); + } + return moveToSideStage(task, sideStagePosition); + } + + public boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @SplitPosition int sideStagePosition) { + return mStageCoordinator.moveToSideStage(task, sideStagePosition); + } + + public boolean removeFromSideStage(int taskId) { + return mStageCoordinator.removeFromSideStage(taskId); + } + + public void setSideStageOutline(boolean enable) { + mStageCoordinator.setSideStageOutline(enable); + } + + public void setSideStagePosition(@SplitPosition int sideStagePosition) { + mStageCoordinator.setSideStagePosition(sideStagePosition, null /* wct */); + } + + public void setSideStageVisibility(boolean visible) { + mStageCoordinator.setSideStageVisibility(visible); + } + + public void enterSplitScreen(int taskId, boolean leftOrTop) { + moveToSideStage(taskId, + leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); + } + + public void exitSplitScreen(int toTopTaskId, int exitReason) { + mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason); + } + + public void onKeyguardOccludedChanged(boolean occluded) { + mStageCoordinator.onKeyguardOccludedChanged(occluded); + } + + public void onKeyguardVisibilityChanged(boolean showing) { + mStageCoordinator.onKeyguardVisibilityChanged(showing); + } + + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mStageCoordinator.exitSplitScreenOnHide(exitSplitScreenOnHide); + } + + public void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + mStageCoordinator.getStageBounds(outTopOrLeftBounds, outBottomOrRightBounds); + } + + public void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mStageCoordinator.registerSplitScreenListener(listener); + } + + public void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mStageCoordinator.unregisterSplitScreenListener(listener); + } + + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) { + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, + null /* wct */); + + try { + ActivityTaskManager.getService().startActivityFromRecents(taskId, options); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to launch task", e); + } + } + + public void startShortcut(String packageName, String shortcutId, @SplitPosition int position, + @Nullable Bundle options, UserHandle user) { + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, + null /* wct */); + + try { + LauncherApps launcherApps = + mContext.getSystemService(LauncherApps.class); + launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */, + options, user); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "Failed to launch shortcut", e); + } + } + + public void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, + @Nullable Bundle options) { + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + startIntentLegacy(intent, fillInIntent, position, options); + return; + } + mStageCoordinator.startIntent(intent, fillInIntent, STAGE_TYPE_UNDEFINED, position, options, + null /* remote */); + } + + private void startIntentLegacy(PendingIntent intent, Intent fillInIntent, + @SplitPosition int position, @Nullable Bundle options) { + LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, + SurfaceControl.Transaction t) { + mStageCoordinator.updateSurfaceBounds(null /* layout */, t); + + if (apps != null) { + for (int i = 0; i < apps.length; ++i) { + if (apps[i].mode == MODE_OPENING) { + t.show(apps[i].leash); + } + } + } + + t.apply(); + if (finishedCallback != null) { + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.e(TAG, "Error finishing legacy transition: ", e); + } + } + } + }; + WindowContainerTransaction wct = new WindowContainerTransaction(); + options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct); + } + + RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, RemoteAnimationTarget[] apps) { + if (!isSplitScreenVisible()) return null; + final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession()) + .setContainerLayer() + .setName("RecentsAnimationSplitTasks") + .setHidden(false) + .setCallsite("SplitScreenController#onGoingtoRecentsLegacy"); + mRootTDAOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, builder); + SurfaceControl sc = builder.build(); + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + + // Ensure that we order these in the parent in the right z-order as their previous order + Arrays.sort(apps, (a1, a2) -> a1.prefixOrderIndex - a2.prefixOrderIndex); + int layer = 1; + for (RemoteAnimationTarget appTarget : apps) { + transaction.reparent(appTarget.leash, sc); + transaction.setPosition(appTarget.leash, appTarget.screenSpaceBounds.left, + appTarget.screenSpaceBounds.top); + transaction.setLayer(appTarget.leash, layer++); + } + transaction.apply(); + transaction.close(); + return new RemoteAnimationTarget[]{ + mStageCoordinator.getDividerBarLegacyTarget(), + mStageCoordinator.getOutlineLegacyTarget()}; + } + + /** + * Sets drag info to be logged when splitscreen is entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mStageCoordinator.logOnDroppedToSplit(position, dragSessionId); + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + TAG); + if (mStageCoordinator != null) { + mStageCoordinator.dump(pw, prefix); + } + } + + /** + * The interface for calls from outside the Shell, within the host process. + */ + @ExternalThread + private class SplitScreenImpl implements SplitScreen { + private ISplitScreenImpl mISplitScreen; + private final ArrayMap<SplitScreenListener, Executor> mExecutors = new ArrayMap<>(); + private final SplitScreenListener mListener = new SplitScreenListener() { + @Override + public void onStagePositionChanged(int stage, int position) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onStagePositionChanged(stage, position); + }); + } + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onTaskStageChanged(taskId, stage, visible); + }); + } + } + + @Override + public void onSplitVisibilityChanged(boolean visible) { + for (int i = 0; i < mExecutors.size(); i++) { + final int index = i; + mExecutors.valueAt(index).execute(() -> { + mExecutors.keyAt(index).onSplitVisibilityChanged(visible); + }); + } + } + }; + + @Override + public ISplitScreen createExternalInterface() { + if (mISplitScreen != null) { + mISplitScreen.invalidate(); + } + mISplitScreen = new ISplitScreenImpl(SplitScreenController.this); + return mISplitScreen; + } + + @Override + public void onKeyguardOccludedChanged(boolean occluded) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardOccludedChanged(occluded); + }); + } + + @Override + public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) { + if (mExecutors.containsKey(listener)) return; + + mMainExecutor.execute(() -> { + if (mExecutors.size() == 0) { + SplitScreenController.this.registerSplitScreenListener(mListener); + } + + mExecutors.put(listener, executor); + }); + + executor.execute(() -> { + mStageCoordinator.sendStatusToListener(listener); + }); + } + + @Override + public void unregisterSplitScreenListener(SplitScreenListener listener) { + mMainExecutor.execute(() -> { + mExecutors.remove(listener); + + if (mExecutors.size() == 0) { + SplitScreenController.this.unregisterSplitScreenListener(mListener); + } + }); + } + + @Override + public void onKeyguardVisibilityChanged(boolean showing) { + mMainExecutor.execute(() -> { + SplitScreenController.this.onKeyguardVisibilityChanged(showing); + }); + } + } + + /** + * The interface for calls from outside the host process. + */ + @BinderThread + private static class ISplitScreenImpl extends ISplitScreen.Stub { + private SplitScreenController mController; + private ISplitScreenListener mListener; + private final SplitScreen.SplitScreenListener mSplitScreenListener = + new SplitScreen.SplitScreenListener() { + @Override + public void onStagePositionChanged(int stage, int position) { + try { + if (mListener != null) { + mListener.onStagePositionChanged(stage, position); + } + } catch (RemoteException e) { + Slog.e(TAG, "onStagePositionChanged", e); + } + } + + @Override + public void onTaskStageChanged(int taskId, int stage, boolean visible) { + try { + if (mListener != null) { + mListener.onTaskStageChanged(taskId, stage, visible); + } + } catch (RemoteException e) { + Slog.e(TAG, "onTaskStageChanged", e); + } + } + }; + private final IBinder.DeathRecipient mListenerDeathRecipient = + new IBinder.DeathRecipient() { + @Override + @BinderThread + public void binderDied() { + final SplitScreenController controller = mController; + controller.getRemoteCallExecutor().execute(() -> { + mListener = null; + controller.unregisterSplitScreenListener(mSplitScreenListener); + }); + } + }; + + public ISplitScreenImpl(SplitScreenController controller) { + mController = controller; + } + + /** + * Invalidates this instance, preventing future calls from updating the controller. + */ + void invalidate() { + mController = null; + } + + @Override + public void registerSplitScreenListener(ISplitScreenListener listener) { + executeRemoteCallWithTaskPermission(mController, "registerSplitScreenListener", + (controller) -> { + if (mListener != null) { + mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } + if (listener != null) { + try { + listener.asBinder().linkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death"); + return; + } + } + mListener = listener; + controller.registerSplitScreenListener(mSplitScreenListener); + }); + } + + @Override + public void unregisterSplitScreenListener(ISplitScreenListener listener) { + executeRemoteCallWithTaskPermission(mController, "unregisterSplitScreenListener", + (controller) -> { + if (mListener != null) { + mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, + 0 /* flags */); + } + mListener = null; + controller.unregisterSplitScreenListener(mSplitScreenListener); + }); + } + + @Override + public void exitSplitScreen(int toTopTaskId) { + executeRemoteCallWithTaskPermission(mController, "exitSplitScreen", + (controller) -> { + controller.exitSplitScreen(toTopTaskId, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__UNKNOWN_EXIT); + }); + } + + @Override + public void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + executeRemoteCallWithTaskPermission(mController, "exitSplitScreenOnHide", + (controller) -> { + controller.exitSplitScreenOnHide(exitSplitScreenOnHide); + }); + } + + @Override + public void setSideStageVisibility(boolean visible) { + executeRemoteCallWithTaskPermission(mController, "setSideStageVisibility", + (controller) -> { + controller.setSideStageVisibility(visible); + }); + } + + @Override + public void removeFromSideStage(int taskId) { + executeRemoteCallWithTaskPermission(mController, "removeFromSideStage", + (controller) -> { + controller.removeFromSideStage(taskId); + }); + } + + @Override + public void startTask(int taskId, int stage, int position, @Nullable Bundle options) { + executeRemoteCallWithTaskPermission(mController, "startTask", + (controller) -> { + controller.startTask(taskId, position, options); + }); + } + + @Override + public void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + RemoteAnimationAdapter adapter) { + executeRemoteCallWithTaskPermission(mController, "startTasks", + (controller) -> controller.mStageCoordinator.startTasksWithLegacyTransition( + mainTaskId, mainOptions, sideTaskId, sideOptions, sidePosition, + adapter)); + } + + @Override + public void startTasks(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, + @SplitPosition int sidePosition, + @Nullable RemoteTransition remoteTransition) { + executeRemoteCallWithTaskPermission(mController, "startTasks", + (controller) -> controller.mStageCoordinator.startTasks(mainTaskId, mainOptions, + sideTaskId, sideOptions, sidePosition, remoteTransition)); + } + + @Override + public void startShortcut(String packageName, String shortcutId, int stage, int position, + @Nullable Bundle options, UserHandle user) { + executeRemoteCallWithTaskPermission(mController, "startShortcut", + (controller) -> { + controller.startShortcut(packageName, shortcutId, position, + options, user); + }); + } + + @Override + public void startIntent(PendingIntent intent, Intent fillInIntent, int stage, int position, + @Nullable Bundle options) { + executeRemoteCallWithTaskPermission(mController, "startIntent", + (controller) -> { + controller.startIntent(intent, fillInIntent, position, options); + }); + } + + @Override + public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel, + RemoteAnimationTarget[] apps) { + final RemoteAnimationTarget[][] out = new RemoteAnimationTarget[][]{null}; + executeRemoteCallWithTaskPermission(mController, "onGoingToRecentsLegacy", + (controller) -> out[0] = controller.onGoingToRecentsLegacy(cancel, apps), + true /* blocking */); + return out[0]; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java new file mode 100644 index 000000000000..af9a5aa501e8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitScreenTransitions.java @@ -0,0 +1,298 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; +import static com.android.wm.shell.transition.Transitions.isOpeningType; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.RemoteTransition; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.transition.OneShotRemoteHandler; +import com.android.wm.shell.transition.Transitions; + +import java.util.ArrayList; + +/** Manages transition animations for split-screen. */ +class SplitScreenTransitions { + private static final String TAG = "SplitScreenTransitions"; + + /** 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; + + private final TransactionPool mTransactionPool; + private final Transitions mTransitions; + private final Runnable mOnFinish; + + IBinder mPendingDismiss = null; + IBinder mPendingEnter = null; + + private IBinder mAnimatingTransition = null; + private OneShotRemoteHandler mRemoteHandler = null; + + private Transitions.TransitionFinishCallback mRemoteFinishCB = (wct, wctCB) -> { + if (wct != null || wctCB != null) { + throw new UnsupportedOperationException("finish transactions not supported yet."); + } + onFinish(); + }; + + /** Keeps track of currently running animations */ + private final ArrayList<Animator> mAnimations = new ArrayList<>(); + + private Transitions.TransitionFinishCallback mFinishCallback = null; + private SurfaceControl.Transaction mFinishTransaction; + + SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions, + @NonNull Runnable onFinishCallback) { + mTransactionPool = pool; + mTransitions = transitions; + mOnFinish = onFinishCallback; + } + + void playAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot) { + mFinishCallback = finishCallback; + mAnimatingTransition = transition; + if (mRemoteHandler != null) { + mRemoteHandler.startAnimation(transition, info, startTransaction, finishTransaction, + mRemoteFinishCB); + mRemoteHandler = null; + return; + } + playInternalAnimation(transition, info, startTransaction, mainRoot, sideRoot); + } + + private void playInternalAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull WindowContainerToken mainRoot, + @NonNull WindowContainerToken sideRoot) { + mFinishTransaction = mTransactionPool.acquire(); + + // Play some place-holder fade animations + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + final SurfaceControl leash = change.getLeash(); + final int mode = info.getChanges().get(i).getMode(); + + if (mode == TRANSIT_CHANGE) { + if (change.getParent() != null) { + // This is probably reparented, so we want the parent to be immediately visible + final TransitionInfo.Change parentChange = info.getChange(change.getParent()); + t.show(parentChange.getLeash()); + t.setAlpha(parentChange.getLeash(), 1.f); + // and then animate this layer outside the parent (since, for example, this is + // the home task animating from fullscreen to part-screen). + t.reparent(leash, info.getRootLeash()); + t.setLayer(leash, info.getChanges().size() - i); + // build the finish reparent/reposition + mFinishTransaction.reparent(leash, parentChange.getLeash()); + mFinishTransaction.setPosition(leash, + change.getEndRelOffset().x, change.getEndRelOffset().y); + } + // TODO(shell-transitions): screenshot here + final Rect startBounds = new Rect(change.getStartAbsBounds()); + if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { + // Dismissing split via snap which means the still-visible task has been + // dragged to its end position at animation start so reflect that here. + startBounds.offsetTo(change.getEndAbsBounds().left, + change.getEndAbsBounds().top); + } + final Rect endBounds = new Rect(change.getEndAbsBounds()); + startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y); + startExampleResizeAnimation(leash, startBounds, endBounds); + } + if (change.getParent() != null) { + continue; + } + + if (transition == mPendingEnter && (mainRoot.equals(change.getContainer()) + || sideRoot.equals(change.getContainer()))) { + t.setWindowCrop(leash, change.getStartAbsBounds().width(), + change.getStartAbsBounds().height()); + } + boolean isOpening = isOpeningType(info.getType()); + if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { + // fade in + startExampleAnimation(leash, true /* show */); + } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) { + // fade out + if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { + // Dismissing via snap-to-top/bottom means that the dismissed task is already + // not-visible (usually cropped to oblivion) so immediately set its alpha to 0 + // and don't animate it so it doesn't pop-in when reparented. + t.setAlpha(leash, 0.f); + } else { + startExampleAnimation(leash, false /* show */); + } + } + } + t.apply(); + onFinish(); + } + + /** Starts a transition to enter split with a remote transition animator. */ + IBinder startEnterTransition(@WindowManager.TransitionType int transitType, + @NonNull WindowContainerTransaction wct, @Nullable RemoteTransition remoteTransition, + @NonNull Transitions.TransitionHandler handler) { + if (remoteTransition != null) { + // Wrapping it for ease-of-use (OneShot handles all the binder linking/death stuff) + mRemoteHandler = new OneShotRemoteHandler( + mTransitions.getMainExecutor(), remoteTransition); + } + final IBinder transition = mTransitions.startTransition(transitType, wct, handler); + mPendingEnter = transition; + if (mRemoteHandler != null) { + mRemoteHandler.setTransition(transition); + } + return transition; + } + + /** Starts a transition for dismissing split after dragging the divider to a screen edge */ + IBinder startSnapToDismiss(@NonNull WindowContainerTransaction wct, + @NonNull Transitions.TransitionHandler handler) { + final IBinder transition = mTransitions.startTransition( + TRANSIT_SPLIT_DISMISS_SNAP, wct, handler); + mPendingDismiss = transition; + return transition; + } + + void onFinish() { + if (!mAnimations.isEmpty()) return; + mOnFinish.run(); + if (mFinishTransaction != null) { + mFinishTransaction.apply(); + mTransactionPool.release(mFinishTransaction); + mFinishTransaction = null; + } + mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + mFinishCallback = null; + if (mAnimatingTransition == mPendingEnter) { + mPendingEnter = null; + } + if (mAnimatingTransition == mPendingDismiss) { + mPendingDismiss = null; + } + mAnimatingTransition = null; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) { + final float end = show ? 1.f : 0.f; + final float start = 1.f - end; + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(start, end); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } + + // TODO(shell-transitions): real animations + private void startExampleResizeAnimation(@NonNull SurfaceControl leash, + @NonNull Rect startBounds, @NonNull Rect endBounds) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setWindowCrop(leash, + (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction), + (int) (startBounds.height() * (1.f - fraction) + + endBounds.height() * fraction)); + transaction.setPosition(leash, + startBounds.left * (1.f - fraction) + endBounds.left * fraction, + startBounds.top * (1.f - fraction) + endBounds.top * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setWindowCrop(leash, 0, 0); + transaction.setPosition(leash, endBounds.left, endBounds.top); + transaction.apply(); + mTransactionPool.release(transaction); + mTransitions.getMainExecutor().execute(() -> { + mAnimations.remove(va); + onFinish(); + }); + }; + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + }); + mAnimations.add(va); + mTransitions.getAnimExecutor().execute(va::start); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java new file mode 100644 index 000000000000..e1850396a5c0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/SplitscreenEventLogger.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; + +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; +import com.android.internal.util.FrameworkStatsLog; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; + +/** + * Helper class that to log Drag & Drop UIEvents for a single session, see also go/uievent + */ +public class SplitscreenEventLogger { + + // Used to generate instance ids for this drag if one is not provided + private final InstanceIdSequence mIdSequence; + + // The instance id for the current splitscreen session (from start to end) + private InstanceId mLoggerSessionId; + + // Drag info + private @SplitPosition int mDragEnterPosition; + private InstanceId mDragEnterSessionId; + + // For deduping async events + private int mLastMainStagePosition = -1; + private int mLastMainStageUid = -1; + private int mLastSideStagePosition = -1; + private int mLastSideStageUid = -1; + private float mLastSplitRatio = -1f; + + public SplitscreenEventLogger() { + mIdSequence = new InstanceIdSequence(Integer.MAX_VALUE); + } + + /** + * Return whether a splitscreen session has started. + */ + public boolean hasStartedSession() { + return mLoggerSessionId != null; + } + + /** + * May be called before logEnter() to indicate that the session was started from a drag. + */ + public void enterRequestedByDrag(@SplitPosition int position, InstanceId dragSessionId) { + mDragEnterPosition = position; + mDragEnterSessionId = dragSessionId; + } + + /** + * Logs when the user enters splitscreen. + */ + public void logEnter(float splitRatio, + @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + mLoggerSessionId = mIdSequence.newInstanceId(); + int enterReason = mDragEnterPosition != SPLIT_POSITION_UNDEFINED + ? getDragEnterReasonFromSplitPosition(mDragEnterPosition, isLandscape) + : SPLITSCREEN_UICHANGED__ENTER_REASON__OVERVIEW; + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + updateSplitRatioState(splitRatio); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__ENTER, + enterReason, + 0 /* exitReason */, + splitRatio, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + mDragEnterSessionId != null ? mDragEnterSessionId.getId() : 0, + mLoggerSessionId.getId()); + } + + /** + * Logs when the user exits splitscreen. Only one of the main or side stages should be + * specified to indicate which position was focused as a part of exiting (both can be unset). + */ + public void logExit(int exitReason, @SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if ((mainStagePosition != SPLIT_POSITION_UNDEFINED + && sideStagePosition != SPLIT_POSITION_UNDEFINED) + || (mainStageUid != 0 && sideStageUid != 0)) { + throw new IllegalArgumentException("Only main or side stage should be set"); + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__EXIT, + 0 /* enterReason */, + exitReason, + 0f /* splitRatio */, + getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid, + getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + + // Reset states + mLoggerSessionId = null; + mDragEnterPosition = SPLIT_POSITION_UNDEFINED; + mDragEnterSessionId = null; + mLastMainStagePosition = -1; + mLastMainStageUid = -1; + mLastSideStagePosition = -1; + mLastSideStageUid = -1; + } + + /** + * Logs when an app in the main stage changes. + */ + public void logMainStageAppChange(@SplitPosition int mainStagePosition, int mainStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, + isLandscape), mainStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + 0 /* sideStagePosition */, + 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when an app in the side stage changes. + */ + public void logSideStageAppChange(@SplitPosition int sideStagePosition, int sideStageUid, + boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (!updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, + isLandscape), sideStageUid)) { + // Ignore if there are no user perceived changes + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__APP_CHANGE, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + 0 /* mainStagePosition */, + 0 /* mainStageUid */, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the splitscreen ratio changes. + */ + public void logResize(float splitRatio) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + if (splitRatio <= 0f || splitRatio >= 1f) { + // Don't bother reporting resizes that end up dismissing the split, that will be logged + // via the exit event + return; + } + if (!updateSplitRatioState(splitRatio)) { + // Ignore if there are no user perceived changes + return; + } + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__RESIZE, + 0 /* enterReason */, + 0 /* exitReason */, + mLastSplitRatio, + 0 /* mainStagePosition */, 0 /* mainStageUid */, + 0 /* sideStagePosition */, 0 /* sideStageUid */, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + /** + * Logs when the apps in splitscreen are swapped. + */ + public void logSwap(@SplitPosition int mainStagePosition, int mainStageUid, + @SplitPosition int sideStagePosition, int sideStageUid, boolean isLandscape) { + if (mLoggerSessionId == null) { + // Ignore changes until we've started logging the session + return; + } + + updateMainStageState(getMainStagePositionFromSplitPosition(mainStagePosition, isLandscape), + mainStageUid); + updateSideStageState(getSideStagePositionFromSplitPosition(sideStagePosition, isLandscape), + sideStageUid); + FrameworkStatsLog.write(FrameworkStatsLog.SPLITSCREEN_UI_CHANGED, + FrameworkStatsLog.SPLITSCREEN_UICHANGED__ACTION__SWAP, + 0 /* enterReason */, + 0 /* exitReason */, + 0f /* splitRatio */, + mLastMainStagePosition, + mLastMainStageUid, + mLastSideStagePosition, + mLastSideStageUid, + 0 /* dragInstanceId */, + mLoggerSessionId.getId()); + } + + private boolean updateMainStageState(int mainStagePosition, int mainStageUid) { + boolean changed = (mLastMainStagePosition != mainStagePosition) + || (mLastMainStageUid != mainStageUid); + if (!changed) { + return false; + } + + mLastMainStagePosition = mainStagePosition; + mLastMainStageUid = mainStageUid; + return true; + } + + private boolean updateSideStageState(int sideStagePosition, int sideStageUid) { + boolean changed = (mLastSideStagePosition != sideStagePosition) + || (mLastSideStageUid != sideStageUid); + if (!changed) { + return false; + } + + mLastSideStagePosition = sideStagePosition; + mLastSideStageUid = sideStageUid; + return true; + } + + private boolean updateSplitRatioState(float splitRatio) { + boolean changed = Float.compare(mLastSplitRatio, splitRatio) != 0; + if (!changed) { + return false; + } + + mLastSplitRatio = splitRatio; + return true; + } + + public int getDragEnterReasonFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__DRAG_BOTTOM; + } + } + + private int getMainStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__MAIN_STAGE_POSITION__BOTTOM; + } + } + + private int getSideStagePositionFromSplitPosition(@SplitPosition int position, + boolean isLandscape) { + if (position == SPLIT_POSITION_UNDEFINED) { + return 0; + } + if (isLandscape) { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__LEFT + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__RIGHT; + } else { + return position == SPLIT_POSITION_TOP_OR_LEFT + ? FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__TOP + : FrameworkStatsLog.SPLITSCREEN_UICHANGED__SIDE_STAGE_POSITION__BOTTOM; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java new file mode 100644 index 000000000000..a17942ff7cff --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageCoordinator.java @@ -0,0 +1,1331 @@ +/* + * Copyright (C) 2020 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.stagesplit; + +import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; +import static android.view.WindowManager.TRANSIT_OPEN; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.view.WindowManager.transitTypeToString; +import static android.view.WindowManagerPolicyConstants.SPLIT_DIVIDER_LAYER; + +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME; +import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.stagesplit.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.stagesplit.SplitScreen.stageTypeToString; +import static com.android.wm.shell.stagesplit.SplitScreenTransitions.FLAG_IS_DIVIDER_BAR; +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS_SNAP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; +import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; +import static com.android.wm.shell.transition.Transitions.isClosingType; +import static com.android.wm.shell.transition.Transitions.isOpeningType; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.hardware.devicestate.DeviceStateManager; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.Slog; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; +import android.window.DisplayAreaInfo; +import android.window.RemoteTransition; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.InstanceId; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition; +import com.android.wm.shell.common.split.SplitWindowManager; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.transition.Transitions; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.inject.Provider; + +/** + * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and + * {@link SideStage} stages. + * Some high-level rules: + * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at + * least one child task. + * - The {@link MainStage} should only have children if the coordinator is active. + * - The {@link SplitLayout} divider is only visible if both the {@link MainStage} + * and {@link SideStage} are visible. + * - The {@link MainStage} configuration is fullscreen when the {@link SideStage} isn't visible. + * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and + * {@link #onStageHasChildrenChanged(StageListenerImpl).} + */ +class StageCoordinator implements SplitLayout.SplitLayoutHandler, + RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener, Transitions.TransitionHandler { + + private static final String TAG = StageCoordinator.class.getSimpleName(); + + /** internal value for mDismissTop that represents no dismiss */ + private static final int NO_DISMISS = -2; + + private final SurfaceSession mSurfaceSession = new SurfaceSession(); + + private final MainStage mMainStage; + private final StageListenerImpl mMainStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mMainUnfoldController; + private final SideStage mSideStage; + private final StageListenerImpl mSideStageListener = new StageListenerImpl(); + private final StageTaskUnfoldController mSideUnfoldController; + @SplitPosition + private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; + + private final int mDisplayId; + private SplitLayout mSplitLayout; + private boolean mDividerVisible; + private final SyncTransactionQueue mSyncQueue; + private final RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + private final ShellTaskOrganizer mTaskOrganizer; + private DisplayAreaInfo mDisplayAreaInfo; + private final Context mContext; + private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>(); + private final DisplayImeController mDisplayImeController; + private final DisplayInsetsController mDisplayInsetsController; + private final SplitScreenTransitions mSplitTransitions; + private final SplitscreenEventLogger mLogger; + private boolean mExitSplitScreenOnHide; + private boolean mKeyguardOccluded; + + // TODO(b/187041611): remove this flag after totally deprecated legacy split + /** Whether the device is supporting legacy split or not. */ + private boolean mUseLegacySplit; + + @SplitScreen.StageType private int mDismissTop = NO_DISMISS; + + /** The target stage to dismiss to when unlock after folded. */ + @SplitScreen.StageType private int mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + + private final Runnable mOnTransitionAnimationComplete = () -> { + // If still playing, let it finish. + if (!isSplitScreenVisible()) { + // Update divider state after animation so that it is still around and positioned + // properly for the animation itself. + setDividerVisibility(false); + mSplitLayout.resetDividerPosition(); + } + mDismissTop = NO_DISMISS; + }; + + private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks = + new SplitWindowManager.ParentContainerCallbacks() { + @Override + public void attachToParentSurface(SurfaceControl.Builder b) { + mRootTDAOrganizer.attachToDisplayArea(mDisplayId, b); + } + + @Override + public void onLeashReady(SurfaceControl leash) { + mSyncQueue.runInSync(t -> applyDividerVisibility(t)); + } + }; + + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, Transitions transitions, + TransactionPool transactionPool, SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + mContext = context; + mDisplayId = displayId; + mSyncQueue = syncQueue; + mRootTDAOrganizer = rootTDAOrganizer; + mTaskOrganizer = taskOrganizer; + mLogger = logger; + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + + mMainStage = new MainStage( + mTaskOrganizer, + mDisplayId, + mMainStageListener, + mSyncQueue, + mSurfaceSession, + mMainUnfoldController); + mSideStage = new SideStage( + mContext, + mTaskOrganizer, + mDisplayId, + mSideStageListener, + mSyncQueue, + mSurfaceSession, + mSideUnfoldController); + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSideStage); + mRootTDAOrganizer.registerListener(displayId, this); + final DeviceStateManager deviceStateManager = + mContext.getSystemService(DeviceStateManager.class); + deviceStateManager.registerCallback(taskOrganizer.getExecutor(), + new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged)); + mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, + mOnTransitionAnimationComplete); + transitions.addHandler(this); + } + + @VisibleForTesting + StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, + RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, + MainStage mainStage, SideStage sideStage, DisplayImeController displayImeController, + DisplayInsetsController displayInsetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, + SplitscreenEventLogger logger, + Provider<Optional<StageTaskUnfoldController>> unfoldControllerProvider) { + mContext = context; + mDisplayId = displayId; + mSyncQueue = syncQueue; + mRootTDAOrganizer = rootTDAOrganizer; + mTaskOrganizer = taskOrganizer; + mMainStage = mainStage; + mSideStage = sideStage; + mDisplayImeController = displayImeController; + mDisplayInsetsController = displayInsetsController; + mRootTDAOrganizer.registerListener(displayId, this); + mSplitLayout = splitLayout; + mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions, + mOnTransitionAnimationComplete); + mMainUnfoldController = unfoldControllerProvider.get().orElse(null); + mSideUnfoldController = unfoldControllerProvider.get().orElse(null); + mLogger = logger; + transitions.addHandler(this); + } + + @VisibleForTesting + SplitScreenTransitions getSplitTransitions() { + return mSplitTransitions; + } + + boolean isSplitScreenVisible() { + return mSideStageListener.mVisible && mMainStageListener.mVisible; + } + + boolean moveToSideStage(ActivityManager.RunningTaskInfo task, + @SplitPosition int sideStagePosition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + setSideStagePosition(sideStagePosition, wct); + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.addTask(task, getSideStageBounds(), wct); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(null /* layout */, t)); + return true; + } + + boolean removeFromSideStage(int taskId) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + + /** + * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the + * {@link SideStage} no longer has children. + */ + final boolean result = mSideStage.removeTask(taskId, + mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null, + wct); + mTaskOrganizer.applyTransaction(wct); + return result; + } + + void setSideStageOutline(boolean enable) { + mSideStage.enableOutline(enable); + } + + /** Starts 2 tasks in one transition. */ + void startTasks(int mainTaskId, @Nullable Bundle mainOptions, int sideTaskId, + @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + @Nullable RemoteTransition remoteTransition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mainOptions = mainOptions != null ? mainOptions : new Bundle(); + sideOptions = sideOptions != null ? sideOptions : new Bundle(); + setSideStagePosition(sidePosition, wct); + + // Build a request WCT that will launch both apps such that task 0 is on the main stage + // while task 1 is on the side stage. + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + + // Make sure the launch options will put tasks in the corresponding split roots + addActivityOptions(mainOptions, mMainStage); + addActivityOptions(sideOptions, mSideStage); + + // Add task launch requests + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + + mSplitTransitions.startEnterTransition( + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, wct, remoteTransition, this); + } + + /** Starts 2 tasks in one legacy transition. */ + void startTasksWithLegacyTransition(int mainTaskId, @Nullable Bundle mainOptions, + int sideTaskId, @Nullable Bundle sideOptions, @SplitPosition int sidePosition, + RemoteAnimationAdapter adapter) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Need to add another wrapper here in shell so that we can inject the divider bar + // and also manage the process elevation via setRunningRemote + IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { + @Override + public void onAnimationStart(@WindowManager.TransitionOldType int transit, + RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, + RemoteAnimationTarget[] nonApps, + final IRemoteAnimationFinishedCallback finishedCallback) { + RemoteAnimationTarget[] augmentedNonApps = + new RemoteAnimationTarget[nonApps.length + 1]; + for (int i = 0; i < nonApps.length; ++i) { + augmentedNonApps[i] = nonApps[i]; + } + augmentedNonApps[augmentedNonApps.length - 1] = getDividerBarLegacyTarget(); + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( + adapter.getCallingApplication()); + adapter.getRunner().onAnimationStart(transit, apps, wallpapers, nonApps, + finishedCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + + @Override + public void onAnimationCancelled() { + try { + adapter.getRunner().onAnimationCancelled(); + } catch (RemoteException e) { + Slog.e(TAG, "Error starting remote animation", e); + } + } + }; + RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter( + wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay()); + + if (mainOptions == null) { + mainOptions = ActivityOptions.makeRemoteAnimation(wrappedAdapter).toBundle(); + } else { + ActivityOptions mainActivityOptions = ActivityOptions.fromBundle(mainOptions); + mainActivityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter)); + } + + sideOptions = sideOptions != null ? sideOptions : new Bundle(); + setSideStagePosition(sidePosition, wct); + + // Build a request WCT that will launch both apps such that task 0 is on the main stage + // while task 1 is on the side stage. + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + + // Make sure the launch options will put tasks in the corresponding split roots + addActivityOptions(mainOptions, mMainStage); + addActivityOptions(sideOptions, mSideStage); + + // Add task launch requests + wct.startTask(mainTaskId, mainOptions); + wct.startTask(sideTaskId, sideOptions); + + // Using legacy transitions, so we can't use blast sync since it conflicts. + mTaskOrganizer.applyTransaction(wct); + } + + public void startIntent(PendingIntent intent, Intent fillInIntent, + @SplitScreen.StageType int stage, @SplitPosition int position, + @androidx.annotation.Nullable Bundle options, + @Nullable RemoteTransition remoteTransition) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + options = resolveStartStage(stage, position, options, wct); + wct.sendPendingIntent(intent, fillInIntent, options); + mSplitTransitions.startEnterTransition( + TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, wct, remoteTransition, this); + } + + Bundle resolveStartStage(@SplitScreen.StageType int stage, + @SplitPosition int position, @androidx.annotation.Nullable Bundle options, + @androidx.annotation.Nullable WindowContainerTransaction wct) { + switch (stage) { + case STAGE_TYPE_UNDEFINED: { + // Use the stage of the specified position is valid. + if (position != SPLIT_POSITION_UNDEFINED) { + if (position == getSideStagePosition()) { + options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct); + } else { + options = resolveStartStage(STAGE_TYPE_MAIN, position, options, wct); + } + } else { + // Exit split-screen and launch fullscreen since stage wasn't specified. + prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct); + } + break; + } + case STAGE_TYPE_SIDE: { + if (position != SPLIT_POSITION_UNDEFINED) { + setSideStagePosition(position, wct); + } else { + position = getSideStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + case STAGE_TYPE_MAIN: { + if (position != SPLIT_POSITION_UNDEFINED) { + // Set the side stage opposite of what we want to the main stage. + final int sideStagePosition = position == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + setSideStagePosition(sideStagePosition, wct); + } else { + position = getMainStagePosition(); + } + if (options == null) { + options = new Bundle(); + } + updateActivityOptions(options, position); + break; + } + default: + throw new IllegalArgumentException("Unknown stage=" + stage); + } + + return options; + } + + @SplitPosition + int getSideStagePosition() { + return mSideStagePosition; + } + + @SplitPosition + int getMainStagePosition() { + return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT; + } + + void setSideStagePosition(@SplitPosition int sideStagePosition, + @Nullable WindowContainerTransaction wct) { + setSideStagePosition(sideStagePosition, true /* updateBounds */, wct); + } + + private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds, + @Nullable WindowContainerTransaction wct) { + if (mSideStagePosition == sideStagePosition) return; + mSideStagePosition = sideStagePosition; + sendOnStagePositionChanged(); + + if (mSideStageListener.mVisible && updateBounds) { + if (wct == null) { + // onLayoutSizeChanged builds/applies a wct with the contents of updateWindowBounds. + onLayoutSizeChanged(mSplitLayout); + } else { + updateWindowBounds(mSplitLayout, wct); + updateUnfoldBounds(); + } + } + } + + void setSideStageVisibility(boolean visible) { + if (mSideStageListener.mVisible == visible) return; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSideStage.setVisibility(visible, wct); + mTaskOrganizer.applyTransaction(wct); + } + + void onKeyguardOccludedChanged(boolean occluded) { + // Do not exit split directly, because it needs to wait for task info update to determine + // which task should remain on top after split dismissed. + mKeyguardOccluded = occluded; + } + + void onKeyguardVisibilityChanged(boolean showing) { + if (!showing && mMainStage.isActive() + && mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) { + exitSplitScreen(mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage, + SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED); + } + } + + void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) { + mExitSplitScreenOnHide = exitSplitScreenOnHide; + } + + void exitSplitScreen(int toTopTaskId, int exitReason) { + StageTaskListener childrenToTop = null; + if (mMainStage.containsTask(toTopTaskId)) { + childrenToTop = mMainStage; + } else if (mSideStage.containsTask(toTopTaskId)) { + childrenToTop = mSideStage; + } + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + if (childrenToTop != null) { + childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct); + } + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void exitSplitScreen(StageTaskListener childrenToTop, int exitReason) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + applyExitSplitScreen(childrenToTop, wct, exitReason); + } + + private void applyExitSplitScreen( + StageTaskListener childrenToTop, + WindowContainerTransaction wct, int exitReason) { + mSideStage.removeAllTasks(wct, childrenToTop == mSideStage); + mMainStage.deactivate(wct, childrenToTop == mMainStage); + mTaskOrganizer.applyTransaction(wct); + mSyncQueue.runInSync(t -> t + .setWindowCrop(mMainStage.mRootLeash, null) + .setWindowCrop(mSideStage.mRootLeash, null)); + // Hide divider and reset its position. + setDividerVisibility(false); + mSplitLayout.resetDividerPosition(); + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (childrenToTop != null) { + logExitToStage(exitReason, childrenToTop == mMainStage); + } else { + logExit(exitReason); + } + } + + /** + * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates + * an existing WindowContainerTransaction (rather than applying immediately). This is intended + * to be used when exiting split might be bundled with other window operations. + */ + void prepareExitSplitScreen(@SplitScreen.StageType int stageToTop, + @NonNull WindowContainerTransaction wct) { + mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); + mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); + } + + void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { + outTopOrLeftBounds.set(mSplitLayout.getBounds1()); + outBottomOrRightBounds.set(mSplitLayout.getBounds2()); + } + + private void addActivityOptions(Bundle opts, StageTaskListener stage) { + opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, stage.mRootTaskInfo.token); + } + + void updateActivityOptions(Bundle opts, @SplitPosition int position) { + addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage); + } + + void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) { + if (mListeners.contains(listener)) return; + mListeners.add(listener); + sendStatusToListener(listener); + } + + void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) { + mListeners.remove(listener); + } + + void sendStatusToListener(SplitScreen.SplitScreenListener listener) { + listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + listener.onSplitVisibilityChanged(isSplitScreenVisible()); + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } + + private void sendOnStagePositionChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition()); + l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition()); + } + } + + private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, + boolean present, boolean visible) { + int stage; + if (present) { + stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } else { + // No longer on any stage + stage = STAGE_TYPE_UNDEFINED; + } + if (stage == STAGE_TYPE_MAIN) { + mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } else { + mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + + for (int i = mListeners.size() - 1; i >= 0; --i) { + mListeners.get(i).onTaskStageChanged(taskId, stage, visible); + } + } + + private void sendSplitVisibilityChanged() { + for (int i = mListeners.size() - 1; i >= 0; --i) { + final SplitScreen.SplitScreenListener l = mListeners.get(i); + l.onSplitVisibilityChanged(mDividerVisible); + } + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onSplitVisibilityChanged(mDividerVisible); + mSideUnfoldController.onSplitVisibilityChanged(mDividerVisible); + } + } + + private void onStageRootTaskAppeared(StageListenerImpl stageListener) { + if (mMainStageListener.mHasRootTask && mSideStageListener.mHasRootTask) { + mUseLegacySplit = mContext.getResources().getBoolean(R.bool.config_useLegacySplit); + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Make the stages adjacent to each other so they occlude what's behind them. + wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, + true /* moveTogether */); + + // Only sets side stage as launch-adjacent-flag-root when the device is not using legacy + // split to prevent new split behavior confusing users. + if (!mUseLegacySplit) { + wct.setLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); + } + + mTaskOrganizer.applyTransaction(wct); + } + } + + private void onStageRootTaskVanished(StageListenerImpl stageListener) { + if (stageListener == mMainStageListener || stageListener == mSideStageListener) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Deactivate the main stage if it no longer has a root task. + mMainStage.deactivate(wct); + + if (!mUseLegacySplit) { + wct.clearLaunchAdjacentFlagRoot(mSideStage.mRootTaskInfo.token); + } + + mTaskOrganizer.applyTransaction(wct); + } + } + + private void setDividerVisibility(boolean visible) { + if (mDividerVisible == visible) return; + mDividerVisible = visible; + if (visible) { + mSplitLayout.init(); + updateUnfoldBounds(); + } else { + mSplitLayout.release(); + } + sendSplitVisibilityChanged(); + } + + private void onStageVisibilityChanged(StageListenerImpl stageListener) { + final boolean sideStageVisible = mSideStageListener.mVisible; + final boolean mainStageVisible = mMainStageListener.mVisible; + final boolean bothStageVisible = sideStageVisible && mainStageVisible; + final boolean bothStageInvisible = !sideStageVisible && !mainStageVisible; + final boolean sameVisibility = sideStageVisible == mainStageVisible; + // Only add or remove divider when both visible or both invisible to avoid sometimes we only + // got one stage visibility changed for a moment and it will cause flicker. + if (sameVisibility) { + setDividerVisibility(bothStageVisible); + } + + if (bothStageInvisible) { + if (mExitSplitScreenOnHide + // Don't dismiss staged split when both stages are not visible due to sleeping display, + // like the cases keyguard showing or screen off. + || (!mMainStage.mRootTaskInfo.isSleeping && !mSideStage.mRootTaskInfo.isSleeping)) { + exitSplitScreen(null /* childrenToTop */, + SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME); + } + } else if (mKeyguardOccluded) { + // At least one of the stages is visible while keyguard occluded. Dismiss split because + // there's show-when-locked activity showing on top of keyguard. Also make sure the + // task contains show-when-locked activity remains on top after split dismissed. + final StageTaskListener toTop = + mainStageVisible ? mMainStage : (sideStageVisible ? mSideStage : null); + exitSplitScreen(toTop, SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP); + } + + mSyncQueue.runInSync(t -> { + // Same above, we only set root tasks and divider leash visibility when both stage + // change to visible or invisible to avoid flicker. + if (sameVisibility) { + t.setVisibility(mSideStage.mRootLeash, bothStageVisible) + .setVisibility(mMainStage.mRootLeash, bothStageVisible); + applyDividerVisibility(t); + applyOutlineVisibility(t); + } + }); + } + + private void applyDividerVisibility(SurfaceControl.Transaction t) { + final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash(); + if (dividerLeash == null) { + return; + } + + if (mDividerVisible) { + t.show(dividerLeash) + .setLayer(dividerLeash, SPLIT_DIVIDER_LAYER) + .setPosition(dividerLeash, + mSplitLayout.getDividerBounds().left, + mSplitLayout.getDividerBounds().top); + } else { + t.hide(dividerLeash); + } + } + + private void applyOutlineVisibility(SurfaceControl.Transaction t) { + final SurfaceControl outlineLeash = mSideStage.getOutlineLeash(); + if (outlineLeash == null) { + return; + } + + if (mDividerVisible) { + t.show(outlineLeash).setLayer(outlineLeash, SPLIT_DIVIDER_LAYER); + } else { + t.hide(outlineLeash); + } + } + + private void onStageHasChildrenChanged(StageListenerImpl stageListener) { + final boolean hasChildren = stageListener.mHasChildren; + final boolean isSideStage = stageListener == mSideStageListener; + if (!hasChildren) { + if (isSideStage && mMainStageListener.mVisible) { + // Exit to main stage if side stage no longer has children. + exitSplitScreen(mMainStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); + } else if (!isSideStage && mSideStageListener.mVisible) { + // Exit to side stage if main stage no longer has children. + exitSplitScreen(mSideStage, SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED); + } + } else if (isSideStage) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + // Make sure the main stage is active. + mMainStage.activate(getMainStageBounds(), wct); + mSideStage.setBounds(getSideStageBounds(), wct); + mTaskOrganizer.applyTransaction(wct); + } + if (!mLogger.hasStartedSession() && mMainStageListener.mHasChildren + && mSideStageListener.mHasChildren) { + mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), + getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + } + + @VisibleForTesting + IBinder onSnappedToDismissTransition(boolean mainStageToTop) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + prepareExitSplitScreen(mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE, wct); + return mSplitTransitions.startSnapToDismiss(wct, this); + } + + @Override + public void onSnappedToDismiss(boolean bottomOrRight) { + final boolean mainStageToTop = + bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT + : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; + if (ENABLE_SHELL_TRANSITIONS) { + onSnappedToDismissTransition(mainStageToTop); + return; + } + exitSplitScreen(mainStageToTop ? mMainStage : mSideStage, + SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER); + } + + @Override + public void onDoubleTappedDivider() { + setSideStagePosition(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? SPLIT_POSITION_BOTTOM_OR_RIGHT : SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLandscape()); + } + + @Override + public void onLayoutPositionChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + } + + @Override + public void onLayoutSizeChanging(SplitLayout layout) { + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSideStage.setOutlineVisibility(false); + } + + @Override + public void onLayoutSizeChanged(SplitLayout layout) { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + updateWindowBounds(layout, wct); + updateUnfoldBounds(); + mSyncQueue.queue(wct); + mSyncQueue.runInSync(t -> updateSurfaceBounds(layout, t)); + mSideStage.setOutlineVisibility(true); + mLogger.logResize(mSplitLayout.getDividerPositionAsFraction()); + } + + private void updateUnfoldBounds() { + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.onLayoutChanged(getMainStageBounds()); + mSideUnfoldController.onLayoutChanged(getSideStageBounds()); + } + } + + /** + * Populates `wct` with operations that match the split windows to the current layout. + * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied + */ + private void updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); + } + + void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash, + bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer); + } + + @Override + public int getSplitItemPosition(WindowContainerToken token) { + if (token == null) { + return SPLIT_POSITION_UNDEFINED; + } + + if (token.equals(mMainStage.mRootTaskInfo.getToken())) { + return getMainStagePosition(); + } else if (token.equals(mSideStage.mRootTaskInfo.getToken())) { + return getSideStagePosition(); + } + + return SPLIT_POSITION_UNDEFINED; + } + + @Override + public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) { + final StageTaskListener topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + final StageTaskListener bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final WindowContainerTransaction wct = new WindowContainerTransaction(); + layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo, + bottomRightStage.mRootTaskInfo); + mTaskOrganizer.applyTransaction(wct); + } + + @Override + public void onDisplayAreaAppeared(DisplayAreaInfo displayAreaInfo) { + mDisplayAreaInfo = displayAreaInfo; + if (mSplitLayout == null) { + mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext, + mDisplayAreaInfo.configuration, this, mParentContainerCallbacks, + mDisplayImeController, mTaskOrganizer, true /* applyDismissingParallax */); + mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout); + + if (mMainUnfoldController != null && mSideUnfoldController != null) { + mMainUnfoldController.init(); + mSideUnfoldController.init(); + } + } + } + + @Override + public void onDisplayAreaVanished(DisplayAreaInfo displayAreaInfo) { + throw new IllegalStateException("Well that was unexpected..."); + } + + @Override + public void onDisplayAreaInfoChanged(DisplayAreaInfo displayAreaInfo) { + mDisplayAreaInfo = displayAreaInfo; + if (mSplitLayout != null + && mSplitLayout.updateConfiguration(mDisplayAreaInfo.configuration) + && mMainStage.isActive()) { + onLayoutSizeChanged(mSplitLayout); + } + } + + private void onFoldedStateChanged(boolean folded) { + mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED; + if (!folded) return; + + if (mMainStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE; + } + } + + private Rect getSideStageBounds() { + return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); + } + + private Rect getMainStageBounds() { + return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); + } + + /** + * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain + * this task (yet) so this can also be used to identify which stage to put a task into. + */ + private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) { + // TODO(b/184679596): Find a way to either include task-org information in the transition, + // or synchronize task-org callbacks so we can use stage.containsTask + if (mMainStage.mRootTaskInfo != null + && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) { + return mMainStage; + } else if (mSideStage.mRootTaskInfo != null + && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) { + return mSideStage; + } + return null; + } + + @SplitScreen.StageType + private int getStageType(StageTaskListener stage) { + return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @Nullable TransitionRequestInfo request) { + final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask(); + if (triggerTask == null) { + // still want to monitor everything while in split-screen, so return non-null. + return isSplitScreenVisible() ? new WindowContainerTransaction() : null; + } + + WindowContainerTransaction out = null; + final @WindowManager.TransitionType int type = request.getType(); + if (isSplitScreenVisible()) { + // try to handle everything while in split-screen, so return a WCT even if it's empty. + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split" + + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d" + + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type), + mMainStage.getChildCount(), mSideStage.getChildCount()); + out = new WindowContainerTransaction(); + final StageTaskListener stage = getStageOfTask(triggerTask); + if (stage != null) { + // dismiss split if the last task in one of the stages is going away + if (isClosingType(type) && stage.getChildCount() == 1) { + // The top should be the opposite side that is closing: + mDismissTop = getStageType(stage) == STAGE_TYPE_MAIN + ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } + } else { + if (triggerTask.getActivityType() == ACTIVITY_TYPE_HOME && isOpeningType(type)) { + // Going home so dismiss both. + mDismissTop = STAGE_TYPE_UNDEFINED; + } + } + if (mDismissTop != NO_DISMISS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition " + + " deduced Dismiss from request. toTop=%s", + stageTypeToString(mDismissTop)); + prepareExitSplitScreen(mDismissTop, out); + mSplitTransitions.mPendingDismiss = transition; + } + } else { + // Not in split mode, so look for an open into a split stage just so we can whine and + // complain about how this isn't a supported operation. + if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)) { + if (getStageOfTask(triggerTask) != null) { + throw new IllegalStateException("Entering split implicitly with only one task" + + " isn't supported."); + } + } + } + return out; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (transition != mSplitTransitions.mPendingDismiss + && transition != mSplitTransitions.mPendingEnter) { + // Not entering or exiting, so just do some house-keeping and validation. + + // If we're not in split-mode, just abort so something else can handle it. + if (!isSplitScreenVisible()) return false; + + for (int iC = 0; iC < info.getChanges().size(); ++iC) { + final TransitionInfo.Change change = info.getChanges().get(iC); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || !taskInfo.hasParentTask()) continue; + final StageTaskListener stage = getStageOfTask(taskInfo); + if (stage == null) continue; + if (isOpeningType(change.getMode())) { + if (!stage.containsTask(taskInfo.taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called" + + " with " + taskInfo.taskId + " before startAnimation()."); + } + } else if (isClosingType(change.getMode())) { + if (stage.containsTask(taskInfo.taskId)) { + Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called" + + " with " + taskInfo.taskId + " before startAnimation()."); + } + } + } + if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { + // TODO(shell-transitions): Implement a fallback behavior for now. + throw new IllegalStateException("Somehow removed the last task in a stage" + + " outside of a proper transition"); + // This can happen in some pathological cases. For example: + // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C] + // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time + // In this case, the result *should* be that we leave split. + // TODO(b/184679596): Find a way to either include task-org information in + // the transition, or synchronize task-org callbacks. + } + + // Use normal animations. + return false; + } + + boolean shouldAnimate = true; + if (mSplitTransitions.mPendingEnter == transition) { + shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction); + } else if (mSplitTransitions.mPendingDismiss == transition) { + shouldAnimate = startPendingDismissAnimation(transition, info, startTransaction); + } + if (!shouldAnimate) return false; + + mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, + finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + return true; + } + + private boolean startPendingEnterAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + if (info.getType() == TRANSIT_SPLIT_SCREEN_PAIR_OPEN) { + // First, verify that we actually have opened 2 apps in split. + TransitionInfo.Change mainChild = null; + TransitionInfo.Change sideChild = null; + for (int iC = 0; iC < info.getChanges().size(); ++iC) { + final TransitionInfo.Change change = info.getChanges().get(iC); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + if (taskInfo == null || !taskInfo.hasParentTask()) continue; + final @SplitScreen.StageType int stageType = getStageType(getStageOfTask(taskInfo)); + if (stageType == STAGE_TYPE_MAIN) { + mainChild = change; + } else if (stageType == STAGE_TYPE_SIDE) { + sideChild = change; + } + } + if (mainChild == null || sideChild == null) { + throw new IllegalStateException("Launched 2 tasks in split, but didn't receive" + + " 2 tasks in transition. Possibly one of them failed to launch"); + // TODO: fallback logic. Probably start a new transition to exit split before + // applying anything here. Ideally consolidate with transition-merging. + } + + // Update local states (before animating). + setDividerVisibility(true); + setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, false /* updateBounds */, + null /* wct */); + setSplitsVisible(true); + + addDividerBarToTransition(info, t, true /* show */); + + // Make some noise if things aren't totally expected. These states shouldn't effect + // transitions locally, but remotes (like Launcher) may get confused if they were + // depending on listener callbacks. This can happen because task-organizer callbacks + // aren't serialized with transition callbacks. + // TODO(b/184679596): Find a way to either include task-org information in + // the transition, or synchronize task-org callbacks. + if (!mMainStage.containsTask(mainChild.getTaskInfo().taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + mMainStage + + " to have been called with " + mainChild.getTaskInfo().taskId + + " before startAnimation()."); + } + if (!mSideStage.containsTask(sideChild.getTaskInfo().taskId)) { + Log.w(TAG, "Expected onTaskAppeared on " + mSideStage + + " to have been called with " + sideChild.getTaskInfo().taskId + + " before startAnimation()."); + } + return true; + } else { + // TODO: other entry method animations + throw new RuntimeException("Unsupported split-entry"); + } + } + + private boolean startPendingDismissAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) { + // Make some noise if things aren't totally expected. These states shouldn't effect + // transitions locally, but remotes (like Launcher) may get confused if they were + // depending on listener callbacks. This can happen because task-organizer callbacks + // aren't serialized with transition callbacks. + // TODO(b/184679596): Find a way to either include task-org information in + // the transition, or synchronize task-org callbacks. + if (mMainStage.getChildCount() != 0) { + final StringBuilder tasksLeft = new StringBuilder(); + for (int i = 0; i < mMainStage.getChildCount(); ++i) { + tasksLeft.append(i != 0 ? ", " : ""); + tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i)); + } + Log.w(TAG, "Expected onTaskVanished on " + mMainStage + + " to have been called with [" + tasksLeft.toString() + + "] before startAnimation()."); + } + if (mSideStage.getChildCount() != 0) { + final StringBuilder tasksLeft = new StringBuilder(); + for (int i = 0; i < mSideStage.getChildCount(); ++i) { + tasksLeft.append(i != 0 ? ", " : ""); + tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i)); + } + Log.w(TAG, "Expected onTaskVanished on " + mSideStage + + " to have been called with [" + tasksLeft.toString() + + "] before startAnimation()."); + } + + // Update local states. + setSplitsVisible(false); + // Wait until after animation to update divider + + if (info.getType() == TRANSIT_SPLIT_DISMISS_SNAP) { + // Reset crops so they don't interfere with subsequent launches + t.setWindowCrop(mMainStage.mRootLeash, null); + t.setWindowCrop(mSideStage.mRootLeash, null); + } + + if (mDismissTop == STAGE_TYPE_UNDEFINED) { + // Going home (dismissing both splits) + + // TODO: Have a proper remote for this. Until then, though, reset state and use the + // normal animation stuff (which falls back to the normal launcher remote). + t.hide(mSplitLayout.getDividerLeash()); + setDividerVisibility(false); + mSplitTransitions.mPendingDismiss = null; + return false; + } + + addDividerBarToTransition(info, t, false /* show */); + // We're dismissing split by moving the other one to fullscreen. + // Since we don't have any animations for this yet, just use the internal example + // animations. + return true; + } + + private void addDividerBarToTransition(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, boolean show) { + final SurfaceControl leash = mSplitLayout.getDividerLeash(); + final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash); + final Rect bounds = mSplitLayout.getDividerBounds(); + barChange.setStartAbsBounds(bounds); + barChange.setEndAbsBounds(bounds); + barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); + barChange.setFlags(FLAG_IS_DIVIDER_BAR); + // Technically this should be order-0, but this is running after layer assignment + // and it's a special case, so just add to end. + info.addChange(barChange); + // Be default, make it visible. The remote animator can adjust alpha if it plans to animate. + if (show) { + t.setAlpha(leash, 1.f); + t.setLayer(leash, SPLIT_DIVIDER_LAYER); + t.setPosition(leash, bounds.left, bounds.top); + t.show(leash); + } + } + + RemoteAnimationTarget getDividerBarLegacyTarget() { + final Rect bounds = mSplitLayout.getDividerBounds(); + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + + RemoteAnimationTarget getOutlineLegacyTarget() { + final Rect bounds = mSideStage.mRootTaskInfo.configuration.windowConfiguration.getBounds(); + // Leverage TYPE_DOCK_DIVIDER type when wrapping outline remote animation target in order to + // distinguish as a split auxiliary target in Launcher. + return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */, + mSideStage.getOutlineLeash(), false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, bounds, bounds, + new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */, + null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER); + } + + @Override + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + TAG + " mDisplayId=" + mDisplayId); + pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible); + pw.println(innerPrefix + "MainStage"); + pw.println(childPrefix + "isActive=" + mMainStage.isActive()); + mMainStageListener.dump(pw, childPrefix); + pw.println(innerPrefix + "SideStage"); + mSideStageListener.dump(pw, childPrefix); + pw.println(innerPrefix + "mSplitLayout=" + mSplitLayout); + } + + /** + * Directly set the visibility of both splits. This assumes hasChildren matches visibility. + * This is intended for batch use, so it assumes other state management logic is already + * handled. + */ + private void setSplitsVisible(boolean visible) { + mMainStageListener.mVisible = mSideStageListener.mVisible = visible; + mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible; + } + + /** + * Sets drag info to be logged when splitscreen is next entered. + */ + public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) { + mLogger.enterRequestedByDrag(position, dragSessionId); + } + + /** + * Logs the exit of splitscreen. + */ + private void logExit(int exitReason) { + mLogger.logExit(exitReason, + SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */, + SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + + /** + * Logs the exit of splitscreen to a specific stage. This must be called before the exit is + * executed. + */ + private void logExitToStage(int exitReason, boolean toMainStage) { + mLogger.logExit(exitReason, + toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED, + toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, + !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED, + !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */, + mSplitLayout.isLandscape()); + } + + class StageListenerImpl implements StageTaskListener.StageListenerCallbacks { + boolean mHasRootTask = false; + boolean mVisible = false; + boolean mHasChildren = false; + + @Override + public void onRootTaskAppeared() { + mHasRootTask = true; + StageCoordinator.this.onStageRootTaskAppeared(this); + } + + @Override + public void onStatusChanged(boolean visible, boolean hasChildren) { + if (!mHasRootTask) return; + + if (mHasChildren != hasChildren) { + mHasChildren = hasChildren; + StageCoordinator.this.onStageHasChildrenChanged(this); + } + if (mVisible != visible) { + mVisible = visible; + StageCoordinator.this.onStageVisibilityChanged(this); + } + } + + @Override + public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) { + StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible); + } + + @Override + public void onRootTaskVanished() { + reset(); + StageCoordinator.this.onStageRootTaskVanished(this); + } + + @Override + public void onNoLongerSupportMultiWindow() { + if (mMainStage.isActive()) { + StageCoordinator.this.exitSplitScreen(null /* childrenToTop */, + SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW); + } + } + + private void reset() { + mHasRootTask = false; + mVisible = false; + mHasChildren = false; + } + + public void dump(@NonNull PrintWriter pw, String prefix) { + pw.println(prefix + "mHasRootTask=" + mHasRootTask); + pw.println(prefix + "mVisible=" + mVisible); + pw.println(prefix + "mHasChildren=" + mHasChildren); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java new file mode 100644 index 000000000000..8b36c9406b15 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskListener.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2020 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.stagesplit; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; + +import android.annotation.CallSuper; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.window.WindowContainerTransaction; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.SurfaceUtils; +import com.android.wm.shell.common.SyncTransactionQueue; + +import java.io.PrintWriter; + +/** + * Base class that handle common task org. related for split-screen stages. + * Note that this class and its sub-class do not directly perform hierarchy operations. + * They only serve to hold a collection of tasks and provide APIs like + * {@link #setBounds(Rect, WindowContainerTransaction)} for the centralized {@link StageCoordinator} + * to perform operations in-sync with other containers. + * + * @see StageCoordinator + */ +class StageTaskListener implements ShellTaskOrganizer.TaskListener { + private static final String TAG = StageTaskListener.class.getSimpleName(); + + protected static final int[] CONTROLLED_ACTIVITY_TYPES = {ACTIVITY_TYPE_STANDARD}; + protected static final int[] CONTROLLED_WINDOWING_MODES = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED}; + protected static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE = + {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW}; + + /** Callback interface for listening to changes in a split-screen stage. */ + public interface StageListenerCallbacks { + void onRootTaskAppeared(); + + void onStatusChanged(boolean visible, boolean hasChildren); + + void onChildTaskStatusChanged(int taskId, boolean present, boolean visible); + + void onRootTaskVanished(); + void onNoLongerSupportMultiWindow(); + } + + private final StageListenerCallbacks mCallbacks; + private final SurfaceSession mSurfaceSession; + protected final SyncTransactionQueue mSyncQueue; + + protected ActivityManager.RunningTaskInfo mRootTaskInfo; + protected SurfaceControl mRootLeash; + protected SurfaceControl mDimLayer; + protected SparseArray<ActivityManager.RunningTaskInfo> mChildrenTaskInfo = new SparseArray<>(); + private final SparseArray<SurfaceControl> mChildrenLeashes = new SparseArray<>(); + + private final StageTaskUnfoldController mStageTaskUnfoldController; + + StageTaskListener(ShellTaskOrganizer taskOrganizer, int displayId, + StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, + SurfaceSession surfaceSession, + @Nullable StageTaskUnfoldController stageTaskUnfoldController) { + mCallbacks = callbacks; + mSyncQueue = syncQueue; + mSurfaceSession = surfaceSession; + mStageTaskUnfoldController = stageTaskUnfoldController; + taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + } + + int getChildCount() { + return mChildrenTaskInfo.size(); + } + + boolean containsTask(int taskId) { + return mChildrenTaskInfo.contains(taskId); + } + + /** + * Returns the top activity uid for the top child task. + */ + int getTopChildTaskUid() { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + final ActivityManager.RunningTaskInfo info = mChildrenTaskInfo.valueAt(i); + if (info.topActivityInfo == null) { + continue; + } + return info.topActivityInfo.applicationInfo.uid; + } + return 0; + } + + /** @return {@code true} if this listener contains the currently focused task. */ + boolean isFocused() { + if (mRootTaskInfo == null) { + return false; + } + + if (mRootTaskInfo.isFocused) { + return true; + } + + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + if (mChildrenTaskInfo.valueAt(i).isFocused) { + return true; + } + } + + return false; + } + + @Override + @CallSuper + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + if (mRootTaskInfo == null && !taskInfo.hasParentTask()) { + mRootLeash = leash; + mRootTaskInfo = taskInfo; + mCallbacks.onRootTaskAppeared(); + sendStatusChanged(); + mSyncQueue.runInSync(t -> { + t.hide(mRootLeash); + mDimLayer = + SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession); + }); + } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + final int taskId = taskInfo.taskId; + mChildrenLeashes.put(taskId, leash); + mChildrenTaskInfo.put(taskId, taskInfo); + updateChildTaskSurface(taskInfo, leash, true /* firstAppeared */); + mCallbacks.onChildTaskStatusChanged(taskId, true /* present */, taskInfo.isVisible); + if (ENABLE_SHELL_TRANSITIONS) { + // Status is managed/synchronized by the transition lifecycle. + return; + } + sendStatusChanged(); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskAppeared(taskInfo, leash); + } + } + + @Override + @CallSuper + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (!taskInfo.supportsMultiWindow) { + // Leave split screen if the task no longer supports multi window. + mCallbacks.onNoLongerSupportMultiWindow(); + return; + } + if (mRootTaskInfo.taskId == taskInfo.taskId) { + mRootTaskInfo = taskInfo; + } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) { + mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); + mCallbacks.onChildTaskStatusChanged(taskInfo.taskId, true /* present */, + taskInfo.isVisible); + if (!ENABLE_SHELL_TRANSITIONS) { + updateChildTaskSurface( + taskInfo, mChildrenLeashes.get(taskInfo.taskId), false /* firstAppeared */); + } + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + if (ENABLE_SHELL_TRANSITIONS) { + // Status is managed/synchronized by the transition lifecycle. + return; + } + sendStatusChanged(); + } + + @Override + @CallSuper + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + final int taskId = taskInfo.taskId; + if (mRootTaskInfo.taskId == taskId) { + mCallbacks.onRootTaskVanished(); + mSyncQueue.runInSync(t -> t.remove(mDimLayer)); + mRootTaskInfo = null; + } else if (mChildrenTaskInfo.contains(taskId)) { + mChildrenTaskInfo.remove(taskId); + mChildrenLeashes.remove(taskId); + mCallbacks.onChildTaskStatusChanged(taskId, false /* present */, taskInfo.isVisible); + if (ENABLE_SHELL_TRANSITIONS) { + // Status is managed/synchronized by the transition lifecycle. + return; + } + sendStatusChanged(); + } else { + throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + + "\n mRootTaskInfo: " + mRootTaskInfo); + } + + if (mStageTaskUnfoldController != null) { + mStageTaskUnfoldController.onTaskVanished(taskInfo); + } + } + + @Override + public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { + if (mRootTaskInfo.taskId == taskId) { + b.setParent(mRootLeash); + } else if (mChildrenLeashes.contains(taskId)) { + b.setParent(mChildrenLeashes.get(taskId)); + } else { + throw new IllegalArgumentException("There is no surface for taskId=" + taskId); + } + } + + void setBounds(Rect bounds, WindowContainerTransaction wct) { + wct.setBounds(mRootTaskInfo.token, bounds); + } + + void reorderChild(int taskId, boolean onTop, WindowContainerTransaction wct) { + if (!containsTask(taskId)) { + return; + } + wct.reorder(mChildrenTaskInfo.get(taskId).token, onTop /* onTop */); + } + + void setVisibility(boolean visible, WindowContainerTransaction wct) { + wct.reorder(mRootTaskInfo.token, visible /* onTop */); + } + + void onSplitScreenListenerRegistered(SplitScreen.SplitScreenListener listener, + @SplitScreen.StageType int stage) { + for (int i = mChildrenTaskInfo.size() - 1; i >= 0; --i) { + int taskId = mChildrenTaskInfo.keyAt(i); + listener.onTaskStageChanged(taskId, stage, + mChildrenTaskInfo.get(taskId).isVisible); + } + } + + private void updateChildTaskSurface(ActivityManager.RunningTaskInfo taskInfo, + SurfaceControl leash, boolean firstAppeared) { + final Point taskPositionInParent = taskInfo.positionInParent; + mSyncQueue.runInSync(t -> { + t.setWindowCrop(leash, null); + t.setPosition(leash, taskPositionInParent.x, taskPositionInParent.y); + if (firstAppeared && !ENABLE_SHELL_TRANSITIONS) { + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + } + }); + } + + private void sendStatusChanged() { + mCallbacks.onStatusChanged(mRootTaskInfo.isVisible, mChildrenTaskInfo.size() > 0); + } + + @Override + @CallSuper + public void dump(@NonNull PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + final String childPrefix = innerPrefix + " "; + pw.println(prefix + this); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java new file mode 100644 index 000000000000..62b9da6d4715 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/stagesplit/StageTaskUnfoldController.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2021 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.stagesplit; + +import static android.view.Display.DEFAULT_DISPLAY; + +import android.animation.RectEvaluator; +import android.animation.TypeEvaluator; +import android.annotation.NonNull; +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.util.SparseArray; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; +import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener; +import com.android.wm.shell.unfold.UnfoldBackgroundController; + +import java.util.concurrent.Executor; + +/** + * Controls transformations of the split screen task surfaces in response + * to the unfolding/folding action on foldable devices + */ +public class StageTaskUnfoldController implements UnfoldListener, OnInsetsChangedListener { + + private static final TypeEvaluator<Rect> RECT_EVALUATOR = new RectEvaluator(new Rect()); + private static final float CROPPING_START_MARGIN_FRACTION = 0.05f; + + private final SparseArray<AnimationContext> mAnimationContextByTaskId = new SparseArray<>(); + private final ShellUnfoldProgressProvider mUnfoldProgressProvider; + private final DisplayInsetsController mDisplayInsetsController; + private final UnfoldBackgroundController mBackgroundController; + private final Executor mExecutor; + private final int mExpandedTaskBarHeight; + private final float mWindowCornerRadiusPx; + private final Rect mStageBounds = new Rect(); + private final TransactionPool mTransactionPool; + + private InsetsSource mTaskbarInsetsSource; + private boolean mBothStagesVisible; + + public StageTaskUnfoldController(@NonNull Context context, + @NonNull TransactionPool transactionPool, + @NonNull ShellUnfoldProgressProvider unfoldProgressProvider, + @NonNull DisplayInsetsController displayInsetsController, + @NonNull UnfoldBackgroundController backgroundController, + @NonNull Executor executor) { + mUnfoldProgressProvider = unfoldProgressProvider; + mTransactionPool = transactionPool; + mExecutor = executor; + mBackgroundController = backgroundController; + mDisplayInsetsController = displayInsetsController; + mWindowCornerRadiusPx = ScreenDecorationsUtils.getWindowCornerRadius(context); + mExpandedTaskBarHeight = context.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.taskbar_frame_height); + } + + /** + * Initializes the controller, starts listening for the external events + */ + public void init() { + mUnfoldProgressProvider.addListener(mExecutor, this); + mDisplayInsetsController.addInsetsChangedListener(DEFAULT_DISPLAY, this); + } + + @Override + public void insetsChanged(InsetsState insetsState) { + mTaskbarInsetsSource = insetsState.getSource(InsetsState.ITYPE_EXTRA_NAVIGATION_BAR); + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + /** + * Called when split screen task appeared + * @param taskInfo info for the appeared task + * @param leash surface leash for the appeared task + */ + public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { + AnimationContext context = new AnimationContext(leash); + mAnimationContextByTaskId.put(taskInfo.taskId, context); + } + + /** + * Called when a split screen task vanished + * @param taskInfo info for the vanished task + */ + public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { + AnimationContext context = mAnimationContextByTaskId.get(taskInfo.taskId); + if (context != null) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + resetSurface(transaction, context); + transaction.apply(); + mTransactionPool.release(transaction); + } + mAnimationContextByTaskId.remove(taskInfo.taskId); + } + + @Override + public void onStateChangeProgress(float progress) { + if (mAnimationContextByTaskId.size() == 0 || !mBothStagesVisible) return; + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + mBackgroundController.ensureBackground(transaction); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + AnimationContext context = mAnimationContextByTaskId.valueAt(i); + + context.mCurrentCropRect.set(RECT_EVALUATOR + .evaluate(progress, context.mStartCropRect, context.mEndCropRect)); + + transaction.setWindowCrop(context.mLeash, context.mCurrentCropRect) + .setCornerRadius(context.mLeash, mWindowCornerRadiusPx); + } + + transaction.apply(); + + mTransactionPool.release(transaction); + } + + @Override + public void onStateChangeFinished() { + resetTransformations(); + } + + /** + * Called when split screen visibility changes + * @param bothStagesVisible true if both stages of the split screen are visible + */ + public void onSplitVisibilityChanged(boolean bothStagesVisible) { + mBothStagesVisible = bothStagesVisible; + if (!bothStagesVisible) { + resetTransformations(); + } + } + + /** + * Called when split screen stage bounds changed + * @param bounds new bounds for this stage + */ + public void onLayoutChanged(Rect bounds) { + mStageBounds.set(bounds); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + context.update(); + } + } + + private void resetTransformations() { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + + for (int i = mAnimationContextByTaskId.size() - 1; i >= 0; i--) { + final AnimationContext context = mAnimationContextByTaskId.valueAt(i); + resetSurface(transaction, context); + } + mBackgroundController.removeBackground(transaction); + transaction.apply(); + + mTransactionPool.release(transaction); + } + + private void resetSurface(SurfaceControl.Transaction transaction, AnimationContext context) { + transaction + .setWindowCrop(context.mLeash, null) + .setCornerRadius(context.mLeash, 0.0F); + } + + private class AnimationContext { + final SurfaceControl mLeash; + final Rect mStartCropRect = new Rect(); + final Rect mEndCropRect = new Rect(); + final Rect mCurrentCropRect = new Rect(); + + private AnimationContext(SurfaceControl leash) { + this.mLeash = leash; + update(); + } + + private void update() { + mStartCropRect.set(mStageBounds); + + if (mTaskbarInsetsSource != null) { + // Only insets the cropping window with taskbar when taskbar is expanded + if (mTaskbarInsetsSource.getFrame().height() >= mExpandedTaskBarHeight) { + mStartCropRect.inset(mTaskbarInsetsSource + .calculateVisibleInsets(mStartCropRect)); + } + } + + // Offset to surface coordinates as layout bounds are in screen coordinates + mStartCropRect.offsetTo(0, 0); + + mEndCropRect.set(mStartCropRect); + + int maxSize = Math.max(mEndCropRect.width(), mEndCropRect.height()); + int margin = (int) (maxSize * CROPPING_START_MARGIN_FRACTION); + mStartCropRect.inset(margin, margin, margin, margin); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java index 4e477ca104dd..e7b5744dd21b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashScreenExitAnimation.java @@ -18,6 +18,8 @@ package com.android.wm.shell.startingsurface; import static android.view.Choreographer.CALLBACK_COMMIT; import static android.view.View.GONE; +import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLASHSCREEN_EXIT_ANIM; + import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; @@ -42,6 +44,7 @@ import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.window.SplashScreenView; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.R; import com.android.wm.shell.animation.Interpolators; import com.android.wm.shell.common.TransactionPool; @@ -69,6 +72,7 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { private final int mAppRevealDuration; private final int mAnimationDuration; private final float mIconStartAlpha; + private final float mBrandingStartAlpha; private final TransactionPool mTransactionPool; private ValueAnimator mMainAnimator; @@ -91,9 +95,17 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { || iconView.getLayoutParams().height == 0) { mIconFadeOutDuration = 0; mIconStartAlpha = 0; + mBrandingStartAlpha = 0; mAppRevealDelay = 0; } else { iconView.setLayerType(View.LAYER_TYPE_HARDWARE, null); + // The branding view could only exists when the icon is present. + final View brandingView = view.getBrandingView(); + if (brandingView != null) { + mBrandingStartAlpha = brandingView.getAlpha(); + } else { + mBrandingStartAlpha = 0; + } mIconFadeOutDuration = context.getResources().getInteger( R.integer.starting_window_app_reveal_icon_fade_out_duration); mAppRevealDelay = context.getResources().getInteger( @@ -311,17 +323,19 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { @Override public void onAnimationStart(Animator animation) { - // ignore + InteractionJankMonitor.getInstance().begin(mSplashScreenView, CUJ_SPLASHSCREEN_EXIT_ANIM); } @Override public void onAnimationEnd(Animator animation) { reset(); + InteractionJankMonitor.getInstance().end(CUJ_SPLASHSCREEN_EXIT_ANIM); } @Override public void onAnimationCancel(Animator animation) { reset(); + InteractionJankMonitor.getInstance().cancel(CUJ_SPLASHSCREEN_EXIT_ANIM); } @Override @@ -329,13 +343,21 @@ public class SplashScreenExitAnimation implements Animator.AnimatorListener { // ignore } - private void onAnimationProgress(float linearProgress) { - View iconView = mSplashScreenView.getIconView(); + private void onFadeOutProgress(float linearProgress) { + final float iconProgress = ICON_INTERPOLATOR.getInterpolation( + getProgress(linearProgress, 0 /* delay */, mIconFadeOutDuration)); + final View iconView = mSplashScreenView.getIconView(); + final View brandingView = mSplashScreenView.getBrandingView(); if (iconView != null) { - final float iconProgress = ICON_INTERPOLATOR.getInterpolation( - getProgress(linearProgress, 0 /* delay */, mIconFadeOutDuration)); iconView.setAlpha(mIconStartAlpha * (1 - iconProgress)); } + if (brandingView != null) { + brandingView.setAlpha(mBrandingStartAlpha * (1 - iconProgress)); + } + } + + private void onAnimationProgress(float linearProgress) { + onFadeOutProgress(linearProgress); final float revealLinearProgress = getProgress(linearProgress, mAppRevealDelay, mAppRevealDuration); 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 cdd745ff9794..b191cabcf6aa 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 @@ -110,9 +110,9 @@ public class SplashscreenContentDrawer { @VisibleForTesting final ColorCache mColorCache; - SplashscreenContentDrawer(Context context, TransactionPool pool) { + SplashscreenContentDrawer(Context context, IconProvider iconProvider, TransactionPool pool) { mContext = context; - mIconProvider = new IconProvider(context); + mIconProvider = iconProvider; mTransactionPool = pool; // Initialize Splashscreen worker thread @@ -138,12 +138,14 @@ public class SplashscreenContentDrawer { * null if failed. */ void createContentView(Context context, @StartingWindowType int suggestType, ActivityInfo info, - int taskId, Consumer<SplashScreenView> splashScreenViewConsumer) { + int taskId, Consumer<SplashScreenView> splashScreenViewConsumer, + Consumer<Runnable> uiThreadInitConsumer) { mSplashscreenWorkerHandler.post(() -> { SplashScreenView contentView; try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView"); - contentView = makeSplashScreenContentView(context, info, suggestType); + contentView = makeSplashScreenContentView(context, info, suggestType, + uiThreadInitConsumer); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RuntimeException e) { Slog.w(TAG, "failed creating starting window content at taskId: " @@ -239,7 +241,7 @@ public class SplashscreenContentDrawer { } private SplashScreenView makeSplashScreenContentView(Context context, ActivityInfo ai, - @StartingWindowType int suggestType) { + @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) { updateDensity(); getWindowAttrs(context, mTmpAttrs); @@ -254,6 +256,7 @@ public class SplashscreenContentDrawer { .setWindowBGColor(themeBGColor) .overlayDrawable(legacyDrawable) .chooseStyle(suggestType) + .setUiThreadInitConsumer(uiThreadInitConsumer) .build(); } @@ -324,6 +327,7 @@ public class SplashscreenContentDrawer { private int mThemeColor; private Drawable[] mFinalIconDrawables; private int mFinalIconSize = mIconSize; + private Consumer<Runnable> mUiThreadInitTask; StartingWindowViewBuilder(@NonNull Context context, @NonNull ActivityInfo aInfo) { mContext = context; @@ -345,6 +349,11 @@ public class SplashscreenContentDrawer { return this; } + StartingWindowViewBuilder setUiThreadInitConsumer(Consumer<Runnable> uiThreadInitTask) { + mUiThreadInitTask = uiThreadInitTask; + return this; + } + SplashScreenView build() { Drawable iconDrawable; final int animationDuration; @@ -366,7 +375,7 @@ public class SplashscreenContentDrawer { createIconDrawable(iconDrawable, false); } else { final float iconScale = (float) mIconSize / (float) mDefaultIconSize; - final int densityDpi = mContext.getResources().getDisplayMetrics().densityDpi; + final int densityDpi = mContext.getResources().getConfiguration().densityDpi; final int scaledIconDpi = (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon"); @@ -391,7 +400,8 @@ public class SplashscreenContentDrawer { animationDuration = 0; } - return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration); + return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, animationDuration, + mUiThreadInitTask); } private class ShapeIconFactory extends BaseIconFactory { @@ -469,7 +479,7 @@ public class SplashscreenContentDrawer { } private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable, - int animationDuration) { + int animationDuration, Consumer<Runnable> uiThreadInitTask) { Drawable foreground = null; Drawable background = null; if (iconDrawable != null) { @@ -485,7 +495,8 @@ public class SplashscreenContentDrawer { .setIconSize(iconSize) .setIconBackground(background) .setCenterViewDrawable(foreground) - .setAnimationDurationMillis(animationDuration); + .setAnimationDurationMillis(animationDuration) + .setUiThreadInitConsumer(uiThreadInitTask); if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN && mTmpAttrs.mBrandingImage != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java index 951b97e791c9..709e2219a64e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java @@ -19,6 +19,7 @@ package com.android.wm.shell.startingsurface; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.annotation.NonNull; @@ -38,6 +39,7 @@ import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Trace; +import android.util.Log; import android.util.PathParser; import android.window.SplashScreenView; @@ -50,6 +52,8 @@ import com.android.internal.R; */ public class SplashscreenIconDrawableFactory { + private static final String TAG = "SplashscreenIconDrawableFactory"; + /** * @return An array containing the foreground drawable at index 0 and if needed a background * drawable at index 1. @@ -260,11 +264,12 @@ public class SplashscreenIconDrawableFactory { * A lightweight AdaptiveIconDrawable which support foreground to be Animatable, and keep this * drawable masked by config_icon_mask. */ - private static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable + public static class AnimatableIconAnimateListener extends AdaptiveForegroundDrawable implements SplashScreenView.IconAnimateListener { private Animatable mAnimatableIcon; private Animator mIconAnimator; private boolean mAnimationTriggered; + private AnimatorListenerAdapter mJankMonitoringListener; AnimatableIconAnimateListener(@NonNull Drawable foregroundDrawable) { super(foregroundDrawable); @@ -272,6 +277,11 @@ public class SplashscreenIconDrawableFactory { } @Override + public void setAnimationJankMonitoring(AnimatorListenerAdapter listener) { + mJankMonitoringListener = listener; + } + + @Override public boolean prepareAnimate(long duration, Runnable startListener) { mAnimatableIcon = (Animatable) mForegroundDrawable; mIconAnimator = ValueAnimator.ofInt(0, 1); @@ -282,17 +292,31 @@ public class SplashscreenIconDrawableFactory { if (startListener != null) { startListener.run(); } - mAnimatableIcon.start(); + try { + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationStart(animation); + } + mAnimatableIcon.start(); + } catch (Exception ex) { + Log.e(TAG, "Error while running the splash screen animated icon", ex); + animation.cancel(); + } } @Override public void onAnimationEnd(Animator animation) { mAnimatableIcon.stop(); + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationEnd(animation); + } } @Override public void onAnimationCancel(Animator animation) { mAnimatableIcon.stop(); + if (mJankMonitoringListener != null) { + mJankMonitoringListener.onAnimationCancel(animation); + } } @Override @@ -304,6 +328,14 @@ public class SplashscreenIconDrawableFactory { return true; } + @Override + public void stopAnimation() { + if (mIconAnimator != null && mIconAnimator.isRunning()) { + mIconAnimator.end(); + mJankMonitoringListener = null; + } + } + private final Callback mCallback = new Callback() { @Override public void invalidateDrawable(@NonNull Drawable who) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java index 01c9b6630fa6..76105a39189b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurface.java @@ -36,4 +36,12 @@ public interface StartingSurface { default int getBackgroundColor(TaskInfo taskInfo) { return Color.BLACK; } + + /** Set the proxy to communicate with SysUi side components. */ + void setSysuiProxy(SysuiProxy proxy); + + /** Callback to tell SysUi components execute some methods. */ + interface SysuiProxy { + void requestTopUi(boolean requestTopUi, String componentTag); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java index 4dae63485f8c..270107c01335 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java @@ -37,7 +37,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PixelFormat; -import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.os.IBinder; import android.os.RemoteCallback; @@ -48,19 +47,21 @@ import android.util.Slog; import android.util.SparseArray; import android.view.Choreographer; import android.view.Display; -import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.widget.FrameLayout; import android.window.SplashScreenView; import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; +import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.annotations.ShellSplashscreenThread; @@ -115,20 +116,26 @@ public class StartingSurfaceDrawer { @VisibleForTesting final SplashscreenContentDrawer mSplashscreenContentDrawer; private Choreographer mChoreographer; + private final WindowManagerGlobal mWindowManagerGlobal; + private StartingSurface.SysuiProxy mSysuiProxy; + private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo(); /** * @param splashScreenExecutor The thread used to control add and remove starting window. */ public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, - TransactionPool pool) { + IconProvider iconProvider, TransactionPool pool) { mContext = context; mDisplayManager = mContext.getSystemService(DisplayManager.class); mSplashScreenExecutor = splashScreenExecutor; - mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, pool); + mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, iconProvider, pool); mSplashScreenExecutor.execute(() -> mChoreographer = Choreographer.getInstance()); + mWindowManagerGlobal = WindowManagerGlobal.getInstance(); + mDisplayManager.getDisplay(DEFAULT_DISPLAY); } - private final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); + @VisibleForTesting + final SparseArray<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); /** * Records of {@link SurfaceControlViewHost} where the splash screen icon animation is @@ -147,6 +154,11 @@ public class StartingSurfaceDrawer { : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() : com.android.internal.R.style.Theme_DeviceDefault_DayNight; } + + void setSysuiProxy(StartingSurface.SysuiProxy sysuiProxy) { + mSysuiProxy = sysuiProxy; + } + /** * Called when a task need a splash screen starting window. * @@ -172,7 +184,6 @@ public class StartingSurfaceDrawer { + " theme=" + Integer.toHexString(theme) + " task=" + taskInfo.taskId + " suggestType=" + suggestType); } - final Display display = getDisplay(displayId); if (display == null) { // Can't show splash screen on requested display, so skip showing at all. @@ -319,12 +330,13 @@ public class StartingSurfaceDrawer { } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); }; + if (mSysuiProxy != null) { + mSysuiProxy.requestTopUi(true, TAG); + } mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId, - viewSupplier::setView); - + viewSupplier::setView, viewSupplier::setUiThreadInitTask); try { - final WindowManager wm = context.getSystemService(WindowManager.class); - if (addWindow(taskId, appToken, rootLayout, wm, params, suggestType)) { + if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) { // We use the splash screen worker thread to create SplashScreenView while adding // the window, as otherwise Choreographer#doFrame might be delayed on this thread. // And since Choreographer#doFrame won't happen immediately after adding the window, @@ -357,6 +369,7 @@ public class StartingSurfaceDrawer { private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> { private SplashScreenView mView; private boolean mIsViewSet; + private Runnable mUiThreadInitTask; void setView(SplashScreenView view) { synchronized (this) { mView = view; @@ -365,6 +378,12 @@ public class StartingSurfaceDrawer { } } + void setUiThreadInitTask(Runnable initTask) { + synchronized (this) { + mUiThreadInitTask = initTask; + } + } + @Override public @Nullable SplashScreenView get() { synchronized (this) { @@ -374,6 +393,10 @@ public class StartingSurfaceDrawer { } catch (InterruptedException ignored) { } } + if (mUiThreadInitTask != null) { + mUiThreadInitTask.run(); + mUiThreadInitTask = null; + } return mView; } } @@ -437,12 +460,29 @@ public class StartingSurfaceDrawer { /** * Called when the content of a task is ready to show, starting window can be removed. */ - public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { + if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { + Slog.d(TAG, "Task start finish, remove starting surface for task " + + removalInfo.taskId); + } + removeWindowSynced(removalInfo, false /* immediately */); + } + + /** + * Clear all starting windows immediately. + */ + public void clearAllWindows() { if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { - Slog.d(TAG, "Task start finish, remove starting surface for task " + taskId); + Slog.d(TAG, "Clear all starting windows immediately"); + } + final int taskSize = mStartingWindowRecords.size(); + final int[] taskIds = new int[taskSize]; + for (int i = taskSize - 1; i >= 0; --i) { + taskIds[i] = mStartingWindowRecords.keyAt(i); + } + for (int i = taskSize - 1; i >= 0; --i) { + removeWindowNoAnimate(taskIds[i]); } - removeWindowSynced(taskId, leash, frame, playRevealAnimation); } /** @@ -496,15 +536,17 @@ public class StartingSurfaceDrawer { Slog.v(TAG, reason + "the splash screen. Releasing SurfaceControlViewHost for task:" + taskId); } - viewHost.getView().post(viewHost::release); + SplashScreenView.releaseIconHost(viewHost); } - protected boolean addWindow(int taskId, IBinder appToken, View view, WindowManager wm, + protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, WindowManager.LayoutParams params, @StartingWindowType int suggestType) { boolean shouldSaveView = true; + final Context context = view.getContext(); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView"); - wm.addView(view, params); + mWindowManagerGlobal.addView(view, params, display, + null /* parentWindow */, context.getUserId()); } catch (WindowManager.BadTokenException e) { // ignore Slog.w(TAG, appToken + " already running, starting window not displayed. " @@ -512,9 +554,9 @@ public class StartingSurfaceDrawer { shouldSaveView = false; } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - if (view != null && view.getParent() == null) { + if (view.getParent() == null) { Slog.w(TAG, "view not successfully added to wm, removing view"); - wm.removeViewImmediate(view); + mWindowManagerGlobal.removeView(view, true /* immediate */); shouldSaveView = false; } } @@ -525,7 +567,8 @@ public class StartingSurfaceDrawer { return shouldSaveView; } - private void saveSplashScreenRecord(IBinder appToken, int taskId, View view, + @VisibleForTesting + void saveSplashScreenRecord(IBinder appToken, int taskId, View view, @StartingWindowType int suggestType) { final StartingWindowRecord tView = new StartingWindowRecord(appToken, view, null/* TaskSnapshotWindow */, suggestType); @@ -533,20 +576,20 @@ public class StartingSurfaceDrawer { } private void removeWindowNoAnimate(int taskId) { - removeWindowSynced(taskId, null, null, false); + mTmpRemovalInfo.taskId = taskId; + removeWindowSynced(mTmpRemovalInfo, true /* immediately */); } void onImeDrawnOnTask(int taskId) { final StartingWindowRecord record = mStartingWindowRecords.get(taskId); if (record != null && record.mTaskSnapshotWindow != null && record.mTaskSnapshotWindow.hasImeSurface()) { - record.mTaskSnapshotWindow.removeImmediately(); + removeWindowNoAnimate(taskId); } - mStartingWindowRecords.remove(taskId); } - protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, boolean immediately) { + final int taskId = removalInfo.taskId; final StartingWindowRecord record = mStartingWindowRecords.get(taskId); if (record != null) { if (record.mDecorView != null) { @@ -554,12 +597,13 @@ public class StartingSurfaceDrawer { Slog.v(TAG, "Removing splash screen window for task: " + taskId); } if (record.mContentView != null) { - if (record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { + if (immediately + || record.mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) { removeWindowInner(record.mDecorView, false); } else { - if (playRevealAnimation) { + if (removalInfo.playRevealAnimation) { mSplashscreenContentDrawer.applyExitAnimation(record.mContentView, - leash, frame, + removalInfo.windowAnimationLeash, removalInfo.mainFrame, () -> removeWindowInner(record.mDecorView, true)); } else { // the SplashScreenView has been copied to client, hide the view to skip @@ -578,20 +622,24 @@ public class StartingSurfaceDrawer { if (DEBUG_TASK_SNAPSHOT) { Slog.v(TAG, "Removing task snapshot window for " + taskId); } - record.mTaskSnapshotWindow.scheduleRemove( - () -> mStartingWindowRecords.remove(taskId)); + if (immediately) { + record.mTaskSnapshotWindow.removeImmediately(); + } else { + record.mTaskSnapshotWindow.scheduleRemove(() -> + mStartingWindowRecords.remove(taskId), removalInfo.deferRemoveForIme); + } } } } private void removeWindowInner(View decorView, boolean hideView) { + if (mSysuiProxy != null) { + mSysuiProxy.requestTopUi(false, TAG); + } if (hideView) { decorView.setVisibility(View.GONE); } - final WindowManager wm = decorView.getContext().getSystemService(WindowManager.class); - if (wm != null) { - wm.removeView(decorView); - } + mWindowManagerGlobal.removeView(decorView, false /* immediate */); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java index dee21b093dce..b0a66059a466 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java @@ -28,16 +28,12 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; import android.content.Context; import android.graphics.Color; -import android.graphics.Rect; -import android.os.Build; import android.os.IBinder; -import android.os.RemoteException; import android.os.Trace; -import android.util.Slog; import android.util.SparseIntArray; -import android.view.SurfaceControl; import android.window.StartingWindowInfo; import android.window.StartingWindowInfo.StartingWindowType; +import android.window.StartingWindowRemovalInfo; import android.window.TaskOrganizer; import android.window.TaskSnapshot; @@ -45,8 +41,10 @@ import androidx.annotation.BinderThread; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.TriConsumer; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SingleInstanceRemoteListener; import com.android.wm.shell.common.TransactionPool; /** @@ -68,7 +66,7 @@ import com.android.wm.shell.common.TransactionPool; public class StartingWindowController implements RemoteCallable<StartingWindowController> { private static final String TAG = StartingWindowController.class.getSimpleName(); - public static final boolean DEBUG_SPLASH_SCREEN = Build.isDebuggable(); + public static final boolean DEBUG_SPLASH_SCREEN = false; public static final boolean DEBUG_TASK_SNAPSHOT = false; private static final long TASK_BG_COLOR_RETAIN_TIME_MS = 5000; @@ -87,9 +85,11 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo private final SparseIntArray mTaskBackgroundColors = new SparseIntArray(); public StartingWindowController(Context context, ShellExecutor splashScreenExecutor, - StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, TransactionPool pool) { + StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider, + TransactionPool pool) { mContext = context; - mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, pool); + mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor, + iconProvider, pool); mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm; mSplashScreenExecutor = splashScreenExecutor; } @@ -134,7 +134,7 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken, suggestionType); } else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) { - final TaskSnapshot snapshot = windowInfo.mTaskSnapshot; + final TaskSnapshot snapshot = windowInfo.taskSnapshot; mStartingSurfaceDrawer.makeTaskSnapshotWindow(windowInfo, appToken, snapshot); } @@ -186,18 +186,29 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo /** * Called when the content of a task is ready to show, starting window can be removed. */ - public void removeStartingWindow(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + public void removeStartingWindow(StartingWindowRemovalInfo removalInfo) { mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.removeStartingWindow( - taskId, leash, frame, playRevealAnimation)); + removalInfo)); mSplashScreenExecutor.executeDelayed(() -> { synchronized (mTaskBackgroundColors) { - mTaskBackgroundColors.delete(taskId); + mTaskBackgroundColors.delete(removalInfo.taskId); } }, TASK_BG_COLOR_RETAIN_TIME_MS); } /** + * Clear all starting window immediately, called this method when releasing the task organizer. + */ + public void clearAllWindows() { + mSplashScreenExecutor.execute(() -> { + mStartingSurfaceDrawer.clearAllWindows(); + synchronized (mTaskBackgroundColors) { + mTaskBackgroundColors.clear(); + } + }); + } + + /** * The interface for calls from outside the Shell, within the host process. */ private class StartingSurfaceImpl implements StartingSurface { @@ -224,6 +235,11 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo return color != Color.TRANSPARENT ? color : SplashscreenContentDrawer.getSystemBGColor(); } + + @Override + public void setSysuiProxy(SysuiProxy proxy) { + mSplashScreenExecutor.execute(() -> mStartingSurfaceDrawer.setSysuiProxy(proxy)); + } } /** @@ -232,24 +248,19 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo @BinderThread private static class IStartingWindowImpl extends IStartingWindow.Stub { private StartingWindowController mController; - private IStartingWindowListener mListener; + private SingleInstanceRemoteListener<StartingWindowController, + IStartingWindowListener> mListener; private final TriConsumer<Integer, Integer, Integer> mStartingWindowListener = - this::notifyIStartingWindowListener; - private final IBinder.DeathRecipient mListenerDeathRecipient = - new IBinder.DeathRecipient() { - @Override - @BinderThread - public void binderDied() { - final StartingWindowController controller = mController; - controller.getRemoteCallExecutor().execute(() -> { - mListener = null; - controller.setStartingWindowListener(null); - }); - } + (taskId, supportedType, startingWindowBackgroundColor) -> { + mListener.call(l -> l.onTaskLaunching(taskId, supportedType, + startingWindowBackgroundColor)); }; public IStartingWindowImpl(StartingWindowController controller) { mController = controller; + mListener = new SingleInstanceRemoteListener<>(controller, + c -> c.setStartingWindowListener(mStartingWindowListener), + c -> c.setStartingWindowListener(null)); } /** @@ -263,36 +274,12 @@ public class StartingWindowController implements RemoteCallable<StartingWindowCo public void setStartingWindowListener(IStartingWindowListener listener) { executeRemoteCallWithTaskPermission(mController, "setStartingWindowListener", (controller) -> { - if (mListener != null) { - // Reset the old death recipient - mListener.asBinder().unlinkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } if (listener != null) { - try { - listener.asBinder().linkToDeath(mListenerDeathRecipient, - 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to link to death"); - return; - } + mListener.register(listener); + } else { + mListener.unregister(); } - mListener = listener; - controller.setStartingWindowListener(mStartingWindowListener); }); } - - private void notifyIStartingWindowListener(int taskId, int supportedType, - int startingWindowBackgroundColor) { - if (mListener == null) { - return; - } - - try { - mListener.onTaskLaunching(taskId, supportedType, startingWindowBackgroundColor); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to notify task launching", e); - } - } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index dfb1ae3ef2a0..3e88c464d359 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -130,7 +130,6 @@ public class TaskSnapshotWindow { private final Window mWindow; private final Runnable mClearWindowHandler; - private final long mDelayRemovalTime; private final ShellExecutor mSplashScreenExecutor; private final SurfaceControl mSurfaceControl; private final IWindowSession mSession; @@ -210,7 +209,7 @@ public class TaskSnapshotWindow { final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); - final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0]; + final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); final TaskDescription taskDescription; @@ -221,22 +220,19 @@ public class TaskSnapshotWindow { taskDescription.setBackgroundColor(WHITE); } - final long delayRemovalTime = snapshot.hasImeSurface() ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE - : DELAY_REMOVAL_TIME_GENERAL; - final TaskSnapshotWindow snapshotSurface = new TaskSnapshotWindow( surfaceControl, snapshot, layoutParams.getTitle(), taskDescription, appearance, windowFlags, windowPrivateFlags, taskBounds, orientation, activityType, - delayRemovalTime, topWindowInsetsState, clearWindowHandler, splashScreenExecutor); + topWindowInsetsState, clearWindowHandler, splashScreenExecutor); final Window window = snapshotSurface.mWindow; - final InsetsState mTmpInsetsState = new InsetsState(); + final InsetsState tmpInsetsState = new InsetsState(); final InputChannel tmpInputChannel = new InputChannel(); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#addToDisplay"); final int res = session.addToDisplay(window, layoutParams, View.GONE, displayId, - mTmpInsetsState, tmpInputChannel, mTmpInsetsState, mTempControls); + info.requestedVisibilities, tmpInputChannel, tmpInsetsState, tmpControls); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (res < 0) { Slog.w(TAG, "Failed to add snapshot starting window res=" + res); @@ -249,8 +245,8 @@ public class TaskSnapshotWindow { try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, -1, - tmpFrames, tmpMergedConfiguration, surfaceControl, mTmpInsetsState, - mTempControls, TMP_SURFACE_SIZE); + tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState, + tmpControls, TMP_SURFACE_SIZE); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); @@ -265,9 +261,8 @@ public class TaskSnapshotWindow { public TaskSnapshotWindow(SurfaceControl surfaceControl, TaskSnapshot snapshot, CharSequence title, TaskDescription taskDescription, int appearance, int windowFlags, int windowPrivateFlags, Rect taskBounds, - int currentOrientation, int activityType, long delayRemovalTime, - InsetsState topWindowInsetsState, Runnable clearWindowHandler, - ShellExecutor splashScreenExecutor) { + int currentOrientation, int activityType, InsetsState topWindowInsetsState, + Runnable clearWindowHandler, ShellExecutor splashScreenExecutor) { mSplashScreenExecutor = splashScreenExecutor; mSession = WindowManagerGlobal.getWindowSession(); mWindow = new Window(); @@ -283,7 +278,6 @@ public class TaskSnapshotWindow { mStatusBarColor = taskDescription.getStatusBarColor(); mOrientationOnCreation = currentOrientation; mActivityType = activityType; - mDelayRemovalTime = delayRemovalTime; mTransaction = new SurfaceControl.Transaction(); mClearWindowHandler = clearWindowHandler; mHasImeSurface = snapshot.hasImeSurface(); @@ -294,7 +288,7 @@ public class TaskSnapshotWindow { } boolean hasImeSurface() { - return mHasImeSurface; + return mHasImeSurface; } /** @@ -314,7 +308,7 @@ public class TaskSnapshotWindow { mSystemBarBackgroundPainter.drawNavigationBarBackground(c); } - void scheduleRemove(Runnable onRemove) { + void scheduleRemove(Runnable onRemove, boolean deferRemoveForIme) { // Show the latest content as soon as possible for unlocking to home. if (mActivityType == ACTIVITY_TYPE_HOME) { removeImmediately(); @@ -329,9 +323,12 @@ public class TaskSnapshotWindow { TaskSnapshotWindow.this.removeImmediately(); onRemove.run(); }; - mSplashScreenExecutor.executeDelayed(mScheduledRunnable, mDelayRemovalTime); + final long delayRemovalTime = mHasImeSurface && deferRemoveForIme + ? MAX_DELAY_REMOVAL_TIME_IME_VISIBLE + : DELAY_REMOVAL_TIME_GENERAL; + mSplashScreenExecutor.executeDelayed(mScheduledRunnable, delayRemovalTime); if (DEBUG) { - Slog.d(TAG, "Defer removing snapshot surface in " + mDelayRemovalTime); + Slog.d(TAG, "Defer removing snapshot surface in " + delayRemovalTime); } } @@ -362,7 +359,7 @@ public class TaskSnapshotWindow { static Rect getSystemBarInsets(Rect frame, InsetsState state) { return state.calculateInsets(frame, WindowInsets.Type.systemBars(), - false /* ignoreVisibility */); + false /* ignoreVisibility */).toRect(); } private void drawSnapshot() { @@ -382,6 +379,7 @@ public class TaskSnapshotWindow { // In case window manager leaks us, make sure we don't retain the snapshot. mSnapshot = null; + mSurfaceControl.release(); } private void drawSizeMatchSnapshot() { @@ -449,6 +447,7 @@ public class TaskSnapshotWindow { mTransaction.setBuffer(mSurfaceControl, background); } mTransaction.apply(); + childSurfaceControl.release(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java index 848eff4b56f3..bde2b5ff4d60 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/phone/PhoneStartingWindowTypeAlgorithm.java @@ -71,23 +71,13 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor + " topIsHome:" + topIsHome); } - final int visibleSplashScreenType = legacySplashScreen - ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN - : STARTING_WINDOW_TYPE_SPLASH_SCREEN; - if (!topIsHome) { - if (!processRunning) { + if (!processRunning || newTask || (taskSwitch && !activityCreated)) { return useEmptySplashScreen ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN - : visibleSplashScreenType; - } - if (newTask) { - return useEmptySplashScreen - ? STARTING_WINDOW_TYPE_EMPTY_SPLASH_SCREEN - : visibleSplashScreenType; - } - if (taskSwitch && !activityCreated) { - return visibleSplashScreenType; + : legacySplashScreen + ? STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN + : STARTING_WINDOW_TYPE_SPLASH_SCREEN; } } if (taskSwitch && allowTaskSnapshot) { @@ -107,7 +97,7 @@ public class PhoneStartingWindowTypeAlgorithm implements StartingWindowTypeAlgor * rotation must be the same). */ private boolean isSnapshotCompatible(StartingWindowInfo windowInfo) { - final TaskSnapshot snapshot = windowInfo.mTaskSnapshot; + final TaskSnapshot snapshot = windowInfo.taskSnapshot; if (snapshot == null) { if (DEBUG_SPLASH_SCREEN || DEBUG_TASK_SNAPSHOT) { Slog.d(TAG, "isSnapshotCompatible no snapshot " + windowInfo.taskInfo.taskId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index c6fb5af7d4be..7abda994bb5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -16,18 +16,38 @@ package com.android.wm.shell.transition; +import static android.app.ActivityOptions.ANIM_CLIP_REVEAL; +import static android.app.ActivityOptions.ANIM_CUSTOM; +import static android.app.ActivityOptions.ANIM_NONE; +import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; +import static android.app.ActivityOptions.ANIM_SCALE_UP; +import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_DOWN; +import static android.app.ActivityOptions.ANIM_THUMBNAIL_SCALE_UP; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_IS_VOICE_INTERACTION; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static android.window.TransitionInfo.isIndependent; + +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE; +import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -35,25 +55,37 @@ import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; +import android.hardware.HardwareBuffer; import android.os.IBinder; +import android.os.SystemProperties; +import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; import android.view.SurfaceControl; +import android.view.SurfaceSession; +import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Transformation; import android.window.TransitionInfo; +import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.AttributeCache; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.util.CounterRotator; import java.util.ArrayList; @@ -61,33 +93,179 @@ import java.util.ArrayList; public class DefaultTransitionHandler implements Transitions.TransitionHandler { private static final int MAX_ANIMATION_DURATION = 3000; + /** + * Restrict ability of activities overriding transition animation in a way such that + * an activity can do it only when the transition happens within a same task. + * + * @see android.app.Activity#overridePendingTransition(int, int) + */ + private static final String DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY = + "persist.wm.disable_custom_task_animation"; + + /** + * @see #DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY + */ + static boolean sDisableCustomTaskAnimationProperty = + SystemProperties.getBoolean(DISABLE_CUSTOM_TASK_ANIMATION_PROPERTY, true); + private final TransactionPool mTransactionPool; + private final DisplayController mDisplayController; + private final Context mContext; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; private final TransitionAnimation mTransitionAnimation; + private final SurfaceSession mSurfaceSession = new SurfaceSession(); + /** Keeps track of the currently-running animations associated with each transition. */ private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); private final Rect mInsets = new Rect(0, 0, 0, 0); private float mTransitionAnimationScaleSetting = 1.0f; - DefaultTransitionHandler(@NonNull TransactionPool transactionPool, Context context, + private final int mCurrentUserId; + + private ScreenRotationAnimation mRotationAnimation; + + DefaultTransitionHandler(@NonNull DisplayController displayController, + @NonNull TransactionPool transactionPool, Context context, @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mDisplayController = displayController; mTransactionPool = transactionPool; + mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG); + mCurrentUserId = UserHandle.myUserId(); AttributeCache.init(context); } + @VisibleForTesting + static boolean isRotationSeamless(@NonNull TransitionInfo info, + DisplayController displayController) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Display is changing, check if it should be seamless."); + boolean checkedDisplayLayout = false; + boolean hasTask = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Only look at changing things. showing/hiding don't need to rotate. + if (change.getMode() != TRANSIT_CHANGE) continue; + + // This container isn't rotating, so we can ignore it. + if (change.getEndRotation() == change.getStartRotation()) continue; + + if ((change.getFlags() & FLAG_IS_DISPLAY) != 0) { + // In the presence of System Alert windows we can not seamlessly rotate. + if ((change.getFlags() & FLAG_DISPLAY_HAS_ALERT_WINDOWS) != 0) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " display has system alert windows, so not seamless."); + return false; + } + } else if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { + if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " wallpaper is participating but isn't seamless."); + return false; + } + } else if (change.getTaskInfo() != null) { + hasTask = true; + // We only enable seamless rotation if all the visible task windows requested it. + if (change.getRotationAnimation() != ROTATION_ANIMATION_SEAMLESS) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " task %s isn't requesting seamless, so not seamless.", + change.getTaskInfo().taskId); + return false; + } + + // This is the only way to get display-id currently, so we will check display + // capabilities here + if (!checkedDisplayLayout) { + // only need to check display once. + checkedDisplayLayout = true; + final DisplayLayout displayLayout = displayController.getDisplayLayout( + change.getTaskInfo().displayId); + // For the upside down rotation we don't rotate seamlessly as the navigation + // bar moves position. Note most apps (using orientation:sensor or user as + // opposed to fullSensor) will not enter the reverse portrait orientation, so + // actually the orientation won't change at all. + int upsideDownRotation = displayLayout.getUpsideDownRotation(); + if (change.getStartRotation() == upsideDownRotation + || change.getEndRotation() == upsideDownRotation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " rotation involves upside-down portrait, so not seamless."); + return false; + } + + // If the navigation bar can't change sides, then it will jump when we change + // orientations and we don't rotate seamlessly - unless that is allowed, eg. + // with gesture navigation where the navbar is low-profile enough that this + // isn't very noticeable. + if (!displayLayout.allowSeamlessRotationDespiteNavBarMoving() + && (!(displayLayout.navigationBarCanMove() + && (change.getStartAbsBounds().width() + != change.getStartAbsBounds().height())))) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + " nav bar changes sides, so not seamless."); + return false; + } + } + } + } + + // ROTATION_ANIMATION_SEAMLESS can only be requested by task. + if (hasTask) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Rotation IS seamless."); + return true; + } + return false; + } + + /** + * Gets the rotation animation for the topmost task. Assumes that seamless is checked + * elsewhere, so it will default SEAMLESS to ROTATE. + */ + private int getRotationAnimation(@NonNull TransitionInfo info) { + // Traverse in top-to-bottom order so that the first task is top-most + for (int i = 0; i < info.getChanges().size(); ++i) { + final TransitionInfo.Change change = info.getChanges().get(i); + + // Only look at changing things. showing/hiding don't need to rotate. + if (change.getMode() != TRANSIT_CHANGE) continue; + + // This container isn't rotating, so we can ignore it. + if (change.getEndRotation() == change.getStartRotation()) continue; + + if (change.getTaskInfo() != null) { + final int anim = change.getRotationAnimation(); + if (anim == ROTATION_ANIMATION_UNSPECIFIED + // Fallback animation for seamless should also be default. + || anim == ROTATION_ANIMATION_SEAMLESS) { + return ROTATION_ANIMATION_ROTATE; + } + return anim; + } + } + return ROTATION_ANIMATION_ROTATE; + } + @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "start default transition animation, info = %s", info); + // If keyguard goes away, we should loadKeyguardExitAnimation. Otherwise this just + // immediately finishes since there is no animation for screen-wake. + if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) { + startTransaction.apply(); + finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); + return true; + } + if (mAnimations.containsKey(transition)) { throw new IllegalStateException("Got a duplicate startAnimation call for " + transition); @@ -95,21 +273,78 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final ArrayList<Animator> animations = new ArrayList<>(); mAnimations.put(transition, animations); + final ArrayMap<WindowContainerToken, CounterRotator> counterRotators = new ArrayMap<>(); + final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; + + for (int i = 0; i < counterRotators.size(); ++i) { + counterRotators.valueAt(i).cleanUp(info.getRootLeash()); + } + counterRotators.clear(); + + if (mRotationAnimation != null) { + mRotationAnimation.kill(); + mRotationAnimation = null; + } + mAnimations.remove(transition); finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); }; + + final int wallpaperTransit = getWallpaperTransitType(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); + + if (change.getMode() == TRANSIT_CHANGE && (change.getFlags() & FLAG_IS_DISPLAY) != 0) { + int rotateDelta = change.getEndRotation() - change.getStartRotation(); + int displayW = change.getEndAbsBounds().width(); + int displayH = change.getEndAbsBounds().height(); + if (info.getType() == TRANSIT_CHANGE) { + boolean isSeamless = isRotationSeamless(info, mDisplayController); + final int anim = getRotationAnimation(info); + if (!(isSeamless || anim == ROTATION_ANIMATION_JUMPCUT)) { + mRotationAnimation = new ScreenRotationAnimation(mContext, mSurfaceSession, + mTransactionPool, startTransaction, change, info.getRootLeash()); + mRotationAnimation.startAnimation(animations, onAnimFinish, + mTransitionAnimationScaleSetting, mMainExecutor, mAnimExecutor); + continue; + } + } else { + // opening/closing an app into a new orientation. Counter-rotate all + // "going-away" things since they are still in the old orientation. + for (int j = info.getChanges().size() - 1; j >= 0; --j) { + final TransitionInfo.Change innerChange = info.getChanges().get(j); + if (!Transitions.isClosingType(innerChange.getMode()) + || !isIndependent(innerChange, info) + || innerChange.getParent() == null) { + continue; + } + CounterRotator crot = counterRotators.get(innerChange.getParent()); + if (crot == null) { + crot = new CounterRotator(); + crot.setup(startTransaction, + info.getChange(innerChange.getParent()).getLeash(), + rotateDelta, displayW, displayH); + if (crot.getSurface() != null) { + int layer = info.getChanges().size() - j; + startTransaction.setLayer(crot.getSurface(), layer); + } + counterRotators.put(innerChange.getParent(), crot); + } + crot.addChild(startTransaction, innerChange.getLeash()); + } + } + } + if (change.getMode() == TRANSIT_CHANGE) { // No default animation for this, so just update bounds/position. - t.setPosition(change.getLeash(), + startTransaction.setPosition(change.getLeash(), change.getEndAbsBounds().left - change.getEndRelOffset().x, change.getEndAbsBounds().top - change.getEndRelOffset().y); if (change.getTaskInfo() != null) { // Skip non-tasks since those usually have null bounds. - t.setWindowCrop(change.getLeash(), + startTransaction.setWindowCrop(change.getLeash(), change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } } @@ -117,12 +352,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { // Don't animate anything that isn't independent. if (!TransitionInfo.isIndependent(change, info)) continue; - Animation a = loadAnimation(info.getType(), info.getFlags(), change); + Animation a = loadAnimation(info, change, wallpaperTransit); if (a != null) { - startAnimInternal(animations, a, change.getLeash(), onAnimFinish); + startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, + mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */); + + if (info.getAnimationOptions() != null) { + attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions()); + } } } - t.apply(); + startTransaction.apply(); + TransitionMetrics.getInstance().reportAnimationStart(transition); // run finish now in-case there are no animations onAnimFinish.run(); return true; @@ -141,87 +382,134 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } @Nullable - private Animation loadAnimation(int type, int flags, TransitionInfo.Change change) { - // TODO(b/178678389): It should handle more type animation here + private Animation loadAnimation(TransitionInfo info, TransitionInfo.Change change, + int wallpaperTransit) { Animation a = null; - final boolean isOpening = Transitions.isOpeningType(type); + final int type = info.getType(); + final int flags = info.getFlags(); final int changeMode = change.getMode(); final int changeFlags = change.getFlags(); + final boolean isOpeningType = Transitions.isOpeningType(type); + final boolean enter = Transitions.isOpeningType(changeMode); + final boolean isTask = change.getTaskInfo() != null; + final TransitionInfo.AnimationOptions options = info.getAnimationOptions(); + final int overrideType = options != null ? options.getType() : ANIM_NONE; + final boolean canCustomContainer = isTask ? !sDisableCustomTaskAnimationProperty : true; - if (type == TRANSIT_RELAUNCH) { - a = mTransitionAnimation.createRelaunchAnimation( - change.getStartAbsBounds(), mInsets, change.getEndAbsBounds()); - } else if (type == TRANSIT_KEYGUARD_GOING_AWAY) { + if (info.isKeyguardGoingAway()) { a = mTransitionAnimation.loadKeyguardExitAnimation(flags, (changeFlags & FLAG_SHOW_WALLPAPER) != 0); } else if (type == TRANSIT_KEYGUARD_UNOCCLUDE) { a = mTransitionAnimation.loadKeyguardUnoccludeAnimation(); - } else if (changeMode == TRANSIT_OPEN && isOpening) { - if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - // This received a transferred starting window, so don't animate - return null; - } - - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */); - } else if (change.getTaskInfo() != null) { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskOpenEnterAnimation); - } else { - a = mTransitionAnimation.loadDefaultAnimationRes( - (changeFlags & FLAG_TRANSLUCENT) == 0 - ? R.anim.activity_open_enter : R.anim.activity_translucent_open_enter); - } - } else if (changeMode == TRANSIT_TO_FRONT && isOpening) { - if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) { - // This received a transferred starting window, so don't animate - return null; - } - - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityOpenAnimation(true /** enter */); - } else { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskToFrontEnterAnimation); - } - } else if (changeMode == TRANSIT_CLOSE && !isOpening) { - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */); - } else if (change.getTaskInfo() != null) { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskCloseExitAnimation); - } else { - a = mTransitionAnimation.loadDefaultAnimationRes( - (changeFlags & FLAG_TRANSLUCENT) == 0 - ? R.anim.activity_close_exit : R.anim.activity_translucent_close_exit); - } - } else if (changeMode == TRANSIT_TO_BACK && !isOpening) { - if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { - a = mTransitionAnimation.loadVoiceActivityExitAnimation(false /** enter */); + } else if ((changeFlags & FLAG_IS_VOICE_INTERACTION) != 0) { + if (isOpeningType) { + a = mTransitionAnimation.loadVoiceActivityOpenAnimation(enter); } else { - a = mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_taskToBackExitAnimation); + a = mTransitionAnimation.loadVoiceActivityExitAnimation(enter); } } else if (changeMode == TRANSIT_CHANGE) { // In the absence of a specific adapter, we just want to keep everything stationary. a = new AlphaAnimation(1.f, 1.f); a.setDuration(TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION); + } else if (type == TRANSIT_RELAUNCH) { + a = mTransitionAnimation.createRelaunchAnimation( + change.getEndAbsBounds(), mInsets, change.getEndAbsBounds()); + } else if (overrideType == ANIM_CUSTOM + && (canCustomContainer || options.getOverrideTaskTransition())) { + a = mTransitionAnimation.loadAnimationRes(options.getPackageName(), enter + ? options.getEnterResId() : options.getExitResId()); + } else if (overrideType == ANIM_OPEN_CROSS_PROFILE_APPS && enter) { + a = mTransitionAnimation.loadCrossProfileAppEnterAnimation(); + } else if (overrideType == ANIM_CLIP_REVEAL) { + a = mTransitionAnimation.createClipRevealAnimationLocked(type, wallpaperTransit, enter, + change.getEndAbsBounds(), change.getEndAbsBounds(), + options.getTransitionBounds()); + } else if (overrideType == ANIM_SCALE_UP) { + a = mTransitionAnimation.createScaleUpAnimationLocked(type, wallpaperTransit, enter, + change.getEndAbsBounds(), options.getTransitionBounds()); + } else if (overrideType == ANIM_THUMBNAIL_SCALE_UP + || overrideType == ANIM_THUMBNAIL_SCALE_DOWN) { + final boolean scaleUp = overrideType == ANIM_THUMBNAIL_SCALE_UP; + a = mTransitionAnimation.createThumbnailEnterExitAnimationLocked(enter, scaleUp, + change.getEndAbsBounds(), type, wallpaperTransit, options.getThumbnail(), + options.getTransitionBounds()); + } else if ((changeFlags & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0 && isOpeningType) { + // This received a transferred starting window, so don't animate + return null; + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_OPEN) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperIntraOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraOpenExitAnimation); + } else if (wallpaperTransit == WALLPAPER_TRANSITION_INTRA_CLOSE) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperIntraCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperIntraCloseExitAnimation); + } else if (wallpaperTransit == WALLPAPER_TRANSITION_OPEN) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperOpenEnterAnimation + : R.styleable.WindowAnimation_wallpaperOpenExitAnimation); + } else if (wallpaperTransit == WALLPAPER_TRANSITION_CLOSE) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_wallpaperCloseEnterAnimation + : R.styleable.WindowAnimation_wallpaperCloseExitAnimation); + } else if (type == TRANSIT_OPEN) { + if (isTask) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskOpenEnterAnimation + : R.styleable.WindowAnimation_taskOpenExitAnimation); + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && enter) { + a = mTransitionAnimation.loadDefaultAnimationRes( + R.anim.activity_translucent_open_enter); + } else { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_activityOpenEnterAnimation + : R.styleable.WindowAnimation_activityOpenExitAnimation); + } + } + } else if (type == TRANSIT_TO_FRONT) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskToFrontEnterAnimation + : R.styleable.WindowAnimation_taskToFrontExitAnimation); + } else if (type == TRANSIT_CLOSE) { + if (isTask) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskCloseEnterAnimation + : R.styleable.WindowAnimation_taskCloseExitAnimation); + } else { + if ((changeFlags & FLAG_TRANSLUCENT) != 0 && !enter) { + a = mTransitionAnimation.loadDefaultAnimationRes( + R.anim.activity_translucent_close_exit); + } else { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_activityCloseEnterAnimation + : R.styleable.WindowAnimation_activityCloseExitAnimation); + } + } + } else if (type == TRANSIT_TO_BACK) { + a = mTransitionAnimation.loadDefaultAnimationAttr(enter + ? R.styleable.WindowAnimation_taskToBackEnterAnimation + : R.styleable.WindowAnimation_taskToBackExitAnimation); } if (a != null) { - Rect start = change.getStartAbsBounds(); - Rect end = change.getEndAbsBounds(); + if (!a.isInitialized()) { + Rect end = change.getEndAbsBounds(); + a.initialize(end.width(), end.height(), end.width(), end.height()); + } a.restrictDuration(MAX_ANIMATION_DURATION); - a.initialize(end.width(), end.height(), start.width(), start.height()); a.scaleCurrentDuration(mTransitionAnimationScaleSetting); } return a; } - private void startAnimInternal(@NonNull ArrayList<Animator> animations, @NonNull Animation anim, - @NonNull SurfaceControl leash, @NonNull Runnable finishCallback) { - final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + static void startSurfaceAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Animation anim, @NonNull SurfaceControl leash, + @NonNull Runnable finishCallback, @NonNull TransactionPool pool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor, + @Nullable Point position) { + final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); final float[] matrix = new float[9]; @@ -231,14 +519,16 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { va.addUpdateListener(animation -> { final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); - applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix); + applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix, + position); }); final Runnable finisher = () -> { - applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix); + applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix, + position); - mTransactionPool.release(transaction); - mMainExecutor.execute(() -> { + pool.release(transaction); + mainExecutor.execute(() -> { animations.remove(va); finishCallback.run(); }); @@ -255,12 +545,116 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } }); animations.add(va); - mAnimExecutor.execute(va::start); + animExecutor.execute(va::start); + } + + private void attachThumbnail(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, TransitionInfo.Change change, + TransitionInfo.AnimationOptions options) { + final boolean isTask = change.getTaskInfo() != null; + final boolean isOpen = Transitions.isOpeningType(change.getMode()); + final boolean isClose = Transitions.isClosingType(change.getMode()); + if (isOpen) { + if (options.getType() == ANIM_OPEN_CROSS_PROFILE_APPS && isTask) { + attachCrossProfileThunmbnailAnimation(animations, finishCallback, change); + } else if (options.getType() == ANIM_THUMBNAIL_SCALE_UP) { + attachThumbnailAnimation(animations, finishCallback, change, options); + } + } else if (isClose && options.getType() == ANIM_THUMBNAIL_SCALE_DOWN) { + attachThumbnailAnimation(animations, finishCallback, change, options); + } + } + + private void attachCrossProfileThunmbnailAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, TransitionInfo.Change change) { + final int thumbnailDrawableRes = change.getTaskInfo().userId == mCurrentUserId + ? R.drawable.ic_account_circle : R.drawable.ic_corp_badge; + final Rect bounds = change.getEndAbsBounds(); + final HardwareBuffer thumbnail = mTransitionAnimation.createCrossProfileAppsThumbnail( + thumbnailDrawableRes, bounds); + if (thumbnail == null) { + return; + } + + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + change.getLeash(), thumbnail, transaction); + final Animation a = + mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds); + if (a == null) { + return; + } + + final Runnable finisher = () -> { + wt.destroy(transaction); + mTransactionPool.release(transaction); + + finishCallback.run(); + }; + a.restrictDuration(MAX_ANIMATION_DURATION); + a.scaleCurrentDuration(mTransitionAnimationScaleSetting); + startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, + mMainExecutor, mAnimExecutor, new Point(bounds.left, bounds.top)); + } + + private void attachThumbnailAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, TransitionInfo.Change change, + TransitionInfo.AnimationOptions options) { + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession, + change.getLeash(), options.getThumbnail(), transaction); + final Rect bounds = change.getEndAbsBounds(); + final int orientation = mContext.getResources().getConfiguration().orientation; + final Animation a = mTransitionAnimation.createThumbnailAspectScaleAnimationLocked(bounds, + mInsets, options.getThumbnail(), orientation, null /* startRect */, + options.getTransitionBounds(), options.getType() == ANIM_THUMBNAIL_SCALE_UP); + + final Runnable finisher = () -> { + wt.destroy(transaction); + mTransactionPool.release(transaction); + + finishCallback.run(); + }; + a.restrictDuration(MAX_ANIMATION_DURATION); + a.scaleCurrentDuration(mTransitionAnimationScaleSetting); + startSurfaceAnimation(animations, a, wt.getSurface(), finisher, mTransactionPool, + mMainExecutor, mAnimExecutor, null /* position */); + } + + private static int getWallpaperTransitType(TransitionInfo info) { + boolean hasOpenWallpaper = false; + boolean hasCloseWallpaper = false; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if ((change.getFlags() & FLAG_SHOW_WALLPAPER) != 0) { + if (Transitions.isOpeningType(change.getMode())) { + hasOpenWallpaper = true; + } else if (Transitions.isClosingType(change.getMode())) { + hasCloseWallpaper = true; + } + } + } + + if (hasOpenWallpaper && hasCloseWallpaper) { + return Transitions.isOpeningType(info.getType()) + ? WALLPAPER_TRANSITION_INTRA_OPEN : WALLPAPER_TRANSITION_INTRA_CLOSE; + } else if (hasOpenWallpaper) { + return WALLPAPER_TRANSITION_OPEN; + } else if (hasCloseWallpaper) { + return WALLPAPER_TRANSITION_CLOSE; + } else { + return WALLPAPER_TRANSITION_NONE; + } } private static void applyTransformation(long time, SurfaceControl.Transaction t, - SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix) { + SurfaceControl leash, Animation anim, Transformation transformation, float[] matrix, + Point position) { anim.getTransformation(time, transformation); + if (position != null) { + transformation.getMatrix().postTranslate(position.x, position.y); + } t.setMatrix(leash, transformation.getMatrix(), matrix); t.setAlpha(leash, transformation.getAlpha()); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl index dffc700a3690..bdcdb63d2cd6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl @@ -16,7 +16,7 @@ package com.android.wm.shell.transition; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionFilter; /** @@ -28,10 +28,10 @@ interface IShellTransitions { * Registers a remote transition handler. */ oneway void registerRemote(in TransitionFilter filter, - in IRemoteTransition remoteTransition) = 1; + in RemoteTransition remoteTransition) = 1; /** * Unregisters a remote transition handler. */ - oneway void unregisterRemote(in IRemoteTransition remoteTransition) = 2; + oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java new file mode 100644 index 000000000000..61e11e877b90 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021 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.transition; + +import android.annotation.NonNull; +import android.os.RemoteException; +import android.view.IRemoteAnimationFinishedCallback; +import android.view.IRemoteAnimationRunner; +import android.view.RemoteAnimationAdapter; +import android.view.RemoteAnimationTarget; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.IWindowContainerTransactionCallback; + +/** + * Utilities and interfaces for transition-like usage on top of the legacy app-transition and + * synctransaction tools. + */ +public class LegacyTransitions { + + /** + * Interface for a "legacy" transition. Effectively wraps a sync callback + remoteAnimation + * into one callback. + */ + public interface ILegacyTransition { + /** + * Called when both the associated sync transaction finishes and the remote animation is + * ready. + */ + void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback, SurfaceControl.Transaction t); + } + + /** + * Makes sure that a remote animation and corresponding sync callback are called together + * such that the sync callback is called first. This assumes that both the callback receiver + * and the remoteanimation are in the same process so that order is preserved on both ends. + */ + public static class LegacyTransition { + private final ILegacyTransition mLegacyTransition; + private int mSyncId = -1; + private SurfaceControl.Transaction mTransaction; + private int mTransit; + private RemoteAnimationTarget[] mApps; + private RemoteAnimationTarget[] mWallpapers; + private RemoteAnimationTarget[] mNonApps; + private IRemoteAnimationFinishedCallback mFinishCallback = null; + private boolean mCancelled = false; + private final SyncCallback mSyncCallback = new SyncCallback(); + private final RemoteAnimationAdapter mAdapter = + new RemoteAnimationAdapter(new RemoteAnimationWrapper(), 0, 0); + + public LegacyTransition(@WindowManager.TransitionType int type, + @NonNull ILegacyTransition legacyTransition) { + mLegacyTransition = legacyTransition; + mTransit = type; + } + + public @WindowManager.TransitionType int getType() { + return mTransit; + } + + public IWindowContainerTransactionCallback getSyncCallback() { + return mSyncCallback; + } + + public RemoteAnimationAdapter getAdapter() { + return mAdapter; + } + + private class SyncCallback extends IWindowContainerTransactionCallback.Stub { + @Override + public void onTransactionReady(int id, SurfaceControl.Transaction t) + throws RemoteException { + mSyncId = id; + mTransaction = t; + checkApply(); + } + } + + private class RemoteAnimationWrapper extends IRemoteAnimationRunner.Stub { + @Override + public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, + RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, + IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException { + mTransit = transit; + mApps = apps; + mWallpapers = wallpapers; + mNonApps = nonApps; + mFinishCallback = finishedCallback; + checkApply(); + } + + @Override + public void onAnimationCancelled() throws RemoteException { + mCancelled = true; + mApps = mWallpapers = mNonApps = null; + checkApply(); + } + } + + + private void checkApply() throws RemoteException { + if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) return; + mLegacyTransition.onAnimationStart(mTransit, mApps, mWallpapers, + mNonApps, mFinishCallback, mTransaction); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 4da6664aa3dc..3e2a0e635a75 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -18,12 +18,15 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityTaskManager; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; +import android.util.Slog; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; @@ -43,10 +46,10 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { private IBinder mTransition = null; /** The remote to delegate animation to */ - private final IRemoteTransition mRemote; + private final RemoteTransition mRemote; public OneShotRemoteHandler(@NonNull ShellExecutor mainExecutor, - @NonNull IRemoteTransition remote) { + @NonNull RemoteTransition remote) { mMainExecutor = mainExecutor; mRemote = remote; } @@ -57,7 +60,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mTransition != transition) return false; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote" @@ -70,19 +74,31 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { }; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { if (mRemote.asBinder() != null) { mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); } - mMainExecutor.execute( - () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); + mMainExecutor.execute(() -> { + if (sct != null) { + finishTransaction.merge(sct); + } + finishCallback.onTransitionFinished(wct, null /* wctCB */); + }); } }; try { if (mRemote.asBinder() != null) { mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */); } - mRemote.startAnimation(transition, info, t, cb); + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( + mRemote.getAppThread()); + } catch (SecurityException e) { + Slog.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" + + " during unit tests"); + } + mRemote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); if (mRemote.asBinder() != null) { @@ -102,13 +118,14 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { mMainExecutor.execute( () -> finishCallback.onTransitionFinished(wct, null /* wctCB */)); } }; try { - mRemote.mergeAnimation(transition, info, t, mergeTarget, cb); + mRemote.getRemoteTransition().mergeAnimation(transition, info, t, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error merging remote transition.", e); } @@ -118,8 +135,9 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { - IRemoteTransition remote = request.getRemoteTransition(); - if (remote != mRemote) return null; + RemoteTransition remote = request.getRemoteTransition(); + IRemoteTransition iRemote = remote != null ? remote.getRemoteTransition() : null; + if (iRemote != mRemote.getRemoteTransition()) return null; mTransition = transition; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" + " for %s: %s", transition, remote); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index 9bfb261fcb85..ece9f47e8788 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -18,6 +18,7 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityTaskManager; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; @@ -27,6 +28,7 @@ import android.util.Slog; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -50,45 +52,33 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { private final ShellExecutor mMainExecutor; /** Includes remotes explicitly requested by, eg, ActivityOptions */ - private final ArrayMap<IBinder, IRemoteTransition> mRequestedRemotes = new ArrayMap<>(); + private final ArrayMap<IBinder, RemoteTransition> mRequestedRemotes = new ArrayMap<>(); /** Ordered by specificity. Last filters will be checked first */ - private final ArrayList<Pair<TransitionFilter, IRemoteTransition>> mFilters = + private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mFilters = new ArrayList<>(); - private final IBinder.DeathRecipient mTransitionDeathRecipient = - new IBinder.DeathRecipient() { - @Override - @BinderThread - public void binderDied() { - mMainExecutor.execute(() -> mFilters.clear()); - } - }; + private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>(); RemoteTransitionHandler(@NonNull ShellExecutor mainExecutor) { mMainExecutor = mainExecutor; } - void addFiltered(TransitionFilter filter, IRemoteTransition remote) { - try { - remote.asBinder().linkToDeath(mTransitionDeathRecipient, 0 /* flags */); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to link to death"); - return; - } + void addFiltered(TransitionFilter filter, RemoteTransition remote) { + handleDeath(remote.asBinder(), null /* finishCallback */); mFilters.add(new Pair<>(filter, remote)); } - void removeFiltered(IRemoteTransition remote) { + void removeFiltered(RemoteTransition remote) { boolean removed = false; for (int i = mFilters.size() - 1; i >= 0; --i) { - if (mFilters.get(i).second == remote) { + if (mFilters.get(i).second.asBinder().equals(remote.asBinder())) { mFilters.remove(i); removed = true; } } if (removed) { - remote.asBinder().unlinkToDeath(mTransitionDeathRecipient, 0 /* flags */); + unhandleDeath(remote.asBinder(), null /* finishCallback */); } } @@ -99,9 +89,10 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - IRemoteTransition pendingRemote = mRequestedRemotes.get(transition); + RemoteTransition pendingRemote = mRequestedRemotes.get(transition); if (pendingRemote == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s doesn't have " + "explicit remote, search filters for match for %s", transition, info); @@ -110,6 +101,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Checking filter %s", mFilters.get(i)); if (mFilters.get(i).first.matches(info)) { + Slog.d(TAG, "Found filter" + mFilters.get(i)); pendingRemote = mFilters.get(i).second; // Add to requested list so that it can be found for merge requests. mRequestedRemotes.put(transition, pendingRemote); @@ -122,36 +114,34 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { if (pendingRemote == null) return false; - final IRemoteTransition remote = pendingRemote; - final IBinder.DeathRecipient remoteDied = () -> { - Log.e(Transitions.TAG, "Remote transition died, finishing"); - mMainExecutor.execute(() -> { - mRequestedRemotes.remove(transition); - finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); - }); - }; + final RemoteTransition remote = pendingRemote; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { - if (remote.asBinder() != null) { - remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); - } + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { + unhandleDeath(remote.asBinder(), finishCallback); mMainExecutor.execute(() -> { + if (sct != null) { + finishTransaction.merge(sct); + } mRequestedRemotes.remove(transition); finishCallback.onTransitionFinished(wct, null /* wctCB */); }); } }; try { - if (remote.asBinder() != null) { - remote.asBinder().linkToDeath(remoteDied, 0 /* flags */); + handleDeath(remote.asBinder(), finishCallback); + try { + ActivityTaskManager.getService().setRunningRemoteTransitionDelegate( + remote.getAppThread()); + } catch (SecurityException e) { + Log.e(Transitions.TAG, "Unable to boost animation thread. This should only happen" + + " during unit tests"); } - remote.startAnimation(transition, info, t, cb); + remote.getRemoteTransition().startAnimation(transition, info, startTransaction, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error running remote transition.", e); - if (remote.asBinder() != null) { - remote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */); - } + unhandleDeath(remote.asBinder(), finishCallback); mRequestedRemotes.remove(transition); mMainExecutor.execute( () -> finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */)); @@ -163,14 +153,15 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget); + final IRemoteTransition remote = mRequestedRemotes.get(mergeTarget).getRemoteTransition(); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt merge %s into %s", transition, remote); if (remote == null) return; IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() { @Override - public void onTransitionFinished(WindowContainerTransaction wct) { + public void onTransitionFinished(WindowContainerTransaction wct, + SurfaceControl.Transaction sct) { mMainExecutor.execute(() -> { if (!mRequestedRemotes.containsKey(mergeTarget)) { Log.e(TAG, "Merged transition finished after it's mergeTarget (the " @@ -193,11 +184,98 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Nullable public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @Nullable TransitionRequestInfo request) { - IRemoteTransition remote = request.getRemoteTransition(); + RemoteTransition remote = request.getRemoteTransition(); if (remote == null) return null; mRequestedRemotes.put(transition, remote); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "RemoteTransition directly requested" + " for %s: %s", transition, remote); return new WindowContainerTransaction(); } + + private void handleDeath(@NonNull IBinder remote, + @Nullable Transitions.TransitionFinishCallback finishCallback) { + synchronized (mDeathHandlers) { + RemoteDeathHandler deathHandler = mDeathHandlers.get(remote); + if (deathHandler == null) { + deathHandler = new RemoteDeathHandler(remote); + try { + remote.linkToDeath(deathHandler, 0 /* flags */); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to link to death"); + return; + } + mDeathHandlers.put(remote, deathHandler); + } + deathHandler.addUser(finishCallback); + } + } + + private void unhandleDeath(@NonNull IBinder remote, + @Nullable Transitions.TransitionFinishCallback finishCallback) { + synchronized (mDeathHandlers) { + RemoteDeathHandler deathHandler = mDeathHandlers.get(remote); + if (deathHandler == null) return; + deathHandler.removeUser(finishCallback); + if (deathHandler.getUserCount() == 0) { + if (!deathHandler.mPendingFinishCallbacks.isEmpty()) { + throw new IllegalStateException("Unhandling death for binder that still has" + + " pending finishCallback(s)."); + } + remote.unlinkToDeath(deathHandler, 0 /* flags */); + mDeathHandlers.remove(remote); + } + } + } + + /** NOTE: binder deaths can alter the filter order */ + private class RemoteDeathHandler implements IBinder.DeathRecipient { + private final IBinder mRemote; + private final ArrayList<Transitions.TransitionFinishCallback> mPendingFinishCallbacks = + new ArrayList<>(); + private int mUsers = 0; + + RemoteDeathHandler(IBinder remote) { + mRemote = remote; + } + + void addUser(@Nullable Transitions.TransitionFinishCallback finishCallback) { + if (finishCallback != null) { + mPendingFinishCallbacks.add(finishCallback); + } + ++mUsers; + } + + void removeUser(@Nullable Transitions.TransitionFinishCallback finishCallback) { + if (finishCallback != null) { + mPendingFinishCallbacks.remove(finishCallback); + } + --mUsers; + } + + int getUserCount() { + return mUsers; + } + + @Override + @BinderThread + public void binderDied() { + mMainExecutor.execute(() -> { + for (int i = mFilters.size() - 1; i >= 0; --i) { + if (mRemote.equals(mFilters.get(i).second.asBinder())) { + mFilters.remove(i); + } + } + for (int i = mRequestedRemotes.size() - 1; i >= 0; --i) { + if (mRemote.equals(mRequestedRemotes.valueAt(i).asBinder())) { + mRequestedRemotes.removeAt(i); + } + } + for (int i = mPendingFinishCallbacks.size() - 1; i >= 0; --i) { + mPendingFinishCallbacks.get(i).onTransitionFinished( + null /* wct */, null /* wctCB */); + } + mPendingFinishCallbacks.clear(); + }); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java new file mode 100644 index 000000000000..13c670a1ab1e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2021 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.transition; + +import static android.hardware.HardwareBuffer.RGBA_8888; +import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT; +import static android.util.RotationUtils.deltaRotation; +import static android.view.WindowManagerPolicyConstants.SCREEN_FREEZE_LAYER_BASE; + +import static com.android.wm.shell.transition.DefaultTransitionHandler.startSurfaceAnimation; +import static com.android.wm.shell.transition.Transitions.TAG; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorSpace; +import android.graphics.GraphicBuffer; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.hardware.HardwareBuffer; +import android.media.Image; +import android.media.ImageReader; +import android.util.Slog; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.SurfaceSession; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.window.TransitionInfo; + +import com.android.internal.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * This class handles the rotation animation when the device is rotated. + * + * <p> + * The screen rotation animation is composed of 4 different part: + * <ul> + * <li> The screenshot: <p> + * A screenshot of the whole screen prior the change of orientation is taken to hide the + * element resizing below. The screenshot is then animated to rotate and cross-fade to + * the new orientation with the content in the new orientation. + * + * <li> The windows on the display: <p>y + * Once the device is rotated, the screen and its content are in the new orientation. The + * animation first rotate the new content into the old orientation to then be able to + * animate to the new orientation + * + * <li> The Background color frame: <p> + * To have the animation seem more seamless, we add a color transitioning background behind the + * exiting and entering layouts. We compute the brightness of the start and end + * layouts and transition from the two brightness values as grayscale underneath the animation + * + * <li> The entering Blackframe: <p> + * The enter Blackframe is similar to the exit Blackframe but is only used when a custom + * rotation animation is used and matches the new content size instead of the screenshot. + * </ul> + */ +class ScreenRotationAnimation { + static final int MAX_ANIMATION_DURATION = 10 * 1000; + + private final Context mContext; + private final TransactionPool mTransactionPool; + private final float[] mTmpFloats = new float[9]; + // Complete transformations being applied. + private final Matrix mSnapshotInitialMatrix = new Matrix(); + /** The leash of display. */ + private final SurfaceControl mSurfaceControl; + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + + private final int mStartWidth; + private final int mStartHeight; + private final int mEndWidth; + private final int mEndHeight; + private final int mStartRotation; + private final int mEndRotation; + + /** This layer contains the actual screenshot that is to be faded out. */ + private SurfaceControl mScreenshotLayer; + /** + * Only used for screen rotation and not custom animations. Layered behind all other layers + * to avoid showing any "empty" spots + */ + private SurfaceControl mBackColorSurface; + /** The leash using to animate screenshot layer. */ + private SurfaceControl mAnimLeash; + private Transaction mTransaction; + + // The current active animation to move from the old to the new rotated + // state. Which animation is run here will depend on the old and new + // rotations. + private Animation mRotateExitAnimation; + private Animation mRotateEnterAnimation; + + /** Intensity of light/whiteness of the layout before rotation occurs. */ + private float mStartLuma; + /** Intensity of light/whiteness of the layout after rotation occurs. */ + private float mEndLuma; + + ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool, + Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash) { + mContext = context; + mTransactionPool = pool; + + mSurfaceControl = change.getLeash(); + mStartWidth = change.getStartAbsBounds().width(); + mStartHeight = change.getStartAbsBounds().height(); + mEndWidth = change.getEndAbsBounds().width(); + mEndHeight = change.getEndAbsBounds().height(); + mStartRotation = change.getStartRotation(); + mEndRotation = change.getEndRotation(); + + mStartBounds.set(change.getStartAbsBounds()); + mEndBounds.set(change.getEndAbsBounds()); + + mAnimLeash = new SurfaceControl.Builder(session) + .setParent(rootLeash) + .setEffectLayer() + .setCallsite("ShellRotationAnimation") + .setName("Animation leash of screenshot rotation") + .build(); + + try { + SurfaceControl.LayerCaptureArgs args = + new SurfaceControl.LayerCaptureArgs.Builder(mSurfaceControl) + .setCaptureSecureLayers(true) + .setAllowProtected(true) + .setSourceCrop(new Rect(0, 0, mStartWidth, mStartHeight)) + .build(); + SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = + SurfaceControl.captureLayers(args); + if (screenshotBuffer == null) { + Slog.w(TAG, "Unable to take screenshot of display"); + return; + } + + mBackColorSurface = new SurfaceControl.Builder(session) + .setParent(rootLeash) + .setColorLayer() + .setCallsite("ShellRotationAnimation") + .setName("BackColorSurface") + .build(); + + mScreenshotLayer = new SurfaceControl.Builder(session) + .setParent(mAnimLeash) + .setBLASTLayer() + .setSecure(screenshotBuffer.containsSecureLayers()) + .setCallsite("ShellRotationAnimation") + .setName("RotationLayer") + .build(); + + HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer(); + mStartLuma = getMedianBorderLuma(hardwareBuffer, screenshotBuffer.getColorSpace()); + + GraphicBuffer buffer = GraphicBuffer.createFromHardwareBuffer( + screenshotBuffer.getHardwareBuffer()); + + t.setLayer(mBackColorSurface, -1); + t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma}); + t.setAlpha(mBackColorSurface, 1); + t.show(mBackColorSurface); + + t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE); + t.setPosition(mAnimLeash, 0, 0); + t.setAlpha(mAnimLeash, 1); + t.show(mAnimLeash); + + t.setBuffer(mScreenshotLayer, buffer); + t.setColorSpace(mScreenshotLayer, screenshotBuffer.getColorSpace()); + t.show(mScreenshotLayer); + + } catch (Surface.OutOfResourcesException e) { + Slog.w(TAG, "Unable to allocate freeze surface", e); + } + + setRotation(t); + t.apply(); + } + + private void setRotation(SurfaceControl.Transaction t) { + // Compute the transformation matrix that must be applied + // to the snapshot to make it stay in the same original position + // with the current screen rotation. + int delta = deltaRotation(mEndRotation, mStartRotation); + createRotationMatrix(delta, mStartWidth, mStartHeight, mSnapshotInitialMatrix); + setRotationTransform(t, mSnapshotInitialMatrix); + } + + private void setRotationTransform(SurfaceControl.Transaction t, Matrix matrix) { + if (mScreenshotLayer == null) { + return; + } + matrix.getValues(mTmpFloats); + float x = mTmpFloats[Matrix.MTRANS_X]; + float y = mTmpFloats[Matrix.MTRANS_Y]; + t.setPosition(mScreenshotLayer, x, y); + t.setMatrix(mScreenshotLayer, + mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y], + mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]); + + t.setAlpha(mScreenshotLayer, (float) 1.0); + t.show(mScreenshotLayer); + } + + /** + * Returns true if animating. + */ + public boolean startAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, float animationScale, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + if (mScreenshotLayer == null) { + // Can't do animation. + return false; + } + + // TODO : Found a way to get right end luma and re-enable color frame animation. + // End luma value is very not stable so it will cause more flicker is we run background + // color frame animation. + //mEndLuma = getLumaOfSurfaceControl(mEndBounds, mSurfaceControl); + + // Figure out how the screen has moved from the original rotation. + int delta = deltaRotation(mEndRotation, mStartRotation); + switch (delta) { /* Counter-Clockwise Rotations */ + case Surface.ROTATION_0: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_0_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.rotation_animation_enter); + break; + case Surface.ROTATION_90: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_plus_90_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_plus_90_enter); + break; + case Surface.ROTATION_180: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_180_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_180_enter); + break; + case Surface.ROTATION_270: + mRotateExitAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_minus_90_exit); + mRotateEnterAnimation = AnimationUtils.loadAnimation(mContext, + R.anim.screen_rotate_minus_90_enter); + break; + } + + mRotateExitAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight); + mRotateExitAnimation.restrictDuration(MAX_ANIMATION_DURATION); + mRotateExitAnimation.scaleCurrentDuration(animationScale); + mRotateEnterAnimation.initialize(mEndWidth, mEndHeight, mStartWidth, mStartHeight); + mRotateEnterAnimation.restrictDuration(MAX_ANIMATION_DURATION); + mRotateEnterAnimation.scaleCurrentDuration(animationScale); + + mTransaction = mTransactionPool.acquire(); + startDisplayRotation(animations, finishCallback, mainExecutor, animExecutor); + startScreenshotRotationAnimation(animations, finishCallback, mainExecutor, animExecutor); + //startColorAnimation(mTransaction, animationScale); + + return true; + } + + private void startDisplayRotation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, + @NonNull ShellExecutor animExecutor) { + startSurfaceAnimation(animations, mRotateEnterAnimation, mSurfaceControl, finishCallback, + mTransactionPool, mainExecutor, animExecutor, null /* position */); + } + + private void startScreenshotRotationAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Runnable finishCallback, @NonNull ShellExecutor mainExecutor, + @NonNull ShellExecutor animExecutor) { + startSurfaceAnimation(animations, mRotateExitAnimation, mAnimLeash, finishCallback, + mTransactionPool, mainExecutor, animExecutor, null /* position */); + } + + private void startColorAnimation(float animationScale, @NonNull ShellExecutor animExecutor) { + int colorTransitionMs = mContext.getResources().getInteger( + R.integer.config_screen_rotation_color_transition); + final float[] rgbTmpFloat = new float[3]; + final int startColor = Color.rgb(mStartLuma, mStartLuma, mStartLuma); + final int endColor = Color.rgb(mEndLuma, mEndLuma, mEndLuma); + final long duration = colorTransitionMs * (long) animationScale; + final Transaction t = mTransactionPool.acquire(); + + final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + // Animation length is already expected to be scaled. + va.overrideDurationScale(1.0f); + va.setDuration(duration); + va.addUpdateListener(animation -> { + final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime()); + final float fraction = currentPlayTime / va.getDuration(); + applyColor(startColor, endColor, rgbTmpFloat, fraction, mBackColorSurface, t); + }); + va.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, + t); + mTransactionPool.release(t); + } + + @Override + public void onAnimationEnd(Animator animation) { + applyColor(startColor, endColor, rgbTmpFloat, 1f /* fraction */, mBackColorSurface, + t); + mTransactionPool.release(t); + } + }); + animExecutor.execute(va::start); + } + + public void kill() { + Transaction t = mTransaction != null ? mTransaction : mTransactionPool.acquire(); + if (mAnimLeash.isValid()) { + t.remove(mAnimLeash); + } + + if (mScreenshotLayer != null) { + if (mScreenshotLayer.isValid()) { + t.remove(mScreenshotLayer); + } + mScreenshotLayer = null; + + if (mBackColorSurface != null) { + if (mBackColorSurface.isValid()) { + t.remove(mBackColorSurface); + } + mBackColorSurface = null; + } + } + t.apply(); + mTransactionPool.release(t); + } + + /** + * Converts the provided {@link HardwareBuffer} and converts it to a bitmap to then sample the + * luminance at the borders of the bitmap + * @return the average luminance of all the pixels at the borders of the bitmap + */ + private static float getMedianBorderLuma(HardwareBuffer hardwareBuffer, ColorSpace colorSpace) { + // Cannot read content from buffer with protected usage. + if (hardwareBuffer == null || hardwareBuffer.getFormat() != RGBA_8888 + || hasProtectedContent(hardwareBuffer)) { + return 0; + } + + ImageReader ir = ImageReader.newInstance(hardwareBuffer.getWidth(), + hardwareBuffer.getHeight(), hardwareBuffer.getFormat(), 1); + ir.getSurface().attachAndQueueBufferWithColorSpace(hardwareBuffer, colorSpace); + Image image = ir.acquireLatestImage(); + if (image == null || image.getPlanes().length == 0) { + return 0; + } + + Image.Plane plane = image.getPlanes()[0]; + ByteBuffer buffer = plane.getBuffer(); + int width = image.getWidth(); + int height = image.getHeight(); + int pixelStride = plane.getPixelStride(); + int rowStride = plane.getRowStride(); + float[] borderLumas = new float[2 * width + 2 * height]; + + // Grab the top and bottom borders + int l = 0; + for (int x = 0; x < width; x++) { + borderLumas[l++] = getPixelLuminance(buffer, x, 0, pixelStride, rowStride); + borderLumas[l++] = getPixelLuminance(buffer, x, height - 1, pixelStride, rowStride); + } + + // Grab the left and right borders + for (int y = 0; y < height; y++) { + borderLumas[l++] = getPixelLuminance(buffer, 0, y, pixelStride, rowStride); + borderLumas[l++] = getPixelLuminance(buffer, width - 1, y, pixelStride, rowStride); + } + + // Cleanup + ir.close(); + + // Oh, is this too simple and inefficient for you? + // How about implementing a O(n) solution? https://en.wikipedia.org/wiki/Median_of_medians + Arrays.sort(borderLumas); + return borderLumas[borderLumas.length / 2]; + } + + /** + * @return whether the hardwareBuffer passed in is marked as protected. + */ + private static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) { + return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT; + } + + private static float getPixelLuminance(ByteBuffer buffer, int x, int y, + int pixelStride, int rowStride) { + int offset = y * rowStride + x * pixelStride; + int pixel = 0; + pixel |= (buffer.get(offset) & 0xff) << 16; // R + pixel |= (buffer.get(offset + 1) & 0xff) << 8; // G + pixel |= (buffer.get(offset + 2) & 0xff); // B + pixel |= (buffer.get(offset + 3) & 0xff) << 24; // A + return Color.valueOf(pixel).luminance(); + } + + /** + * Gets the average border luma by taking a screenshot of the {@param surfaceControl}. + * @see #getMedianBorderLuma(HardwareBuffer, ColorSpace) + */ + private static float getLumaOfSurfaceControl(Rect bounds, SurfaceControl surfaceControl) { + if (surfaceControl == null) { + return 0; + } + + Rect crop = new Rect(0, 0, bounds.width(), bounds.height()); + SurfaceControl.ScreenshotHardwareBuffer buffer = + SurfaceControl.captureLayers(surfaceControl, crop, 1); + if (buffer == null) { + return 0; + } + + return getMedianBorderLuma(buffer.getHardwareBuffer(), buffer.getColorSpace()); + } + + private static void createRotationMatrix(int rotation, int width, int height, + Matrix outMatrix) { + switch (rotation) { + case Surface.ROTATION_0: + outMatrix.reset(); + break; + case Surface.ROTATION_90: + outMatrix.setRotate(90, 0, 0); + outMatrix.postTranslate(height, 0); + break; + case Surface.ROTATION_180: + outMatrix.setRotate(180, 0, 0); + outMatrix.postTranslate(width, height); + break; + case Surface.ROTATION_270: + outMatrix.setRotate(270, 0, 0); + outMatrix.postTranslate(0, width); + break; + } + } + + private static void applyColor(int startColor, int endColor, float[] rgbFloat, + float fraction, SurfaceControl surface, SurfaceControl.Transaction t) { + final int color = (Integer) ArgbEvaluator.getInstance().evaluate(fraction, startColor, + endColor); + Color middleColor = Color.valueOf(color); + rgbFloat[0] = middleColor.red(); + rgbFloat[1] = middleColor.green(); + rgbFloat[2] = middleColor.blue(); + if (surface.isValid()) { + t.setColor(surface, rgbFloat); + } + t.apply(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java index bc42c6e2f12c..b34049d4ec42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java @@ -17,7 +17,7 @@ package com.android.wm.shell.transition; import android.annotation.NonNull; -import android.window.IRemoteTransition; +import android.window.RemoteTransition; import android.window.TransitionFilter; import com.android.wm.shell.common.annotations.ExternalThread; @@ -38,11 +38,11 @@ public interface ShellTransitions { /** * Registers a remote transition. */ - void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition); + default void registerRemote(@NonNull TransitionFilter filter, + @NonNull RemoteTransition remoteTransition) {} /** * Unregisters a remote transition. */ - void unregisterRemote(@NonNull IRemoteTransition remoteTransition); + default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {} } 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 60707ccdca30..804e449decf8 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.transition; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; +import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; @@ -39,10 +40,11 @@ import android.provider.Settings; import android.util.Log; import android.view.SurfaceControl; import android.view.WindowManager; -import android.window.IRemoteTransition; import android.window.ITransitionPlayer; +import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; +import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransactionCallback; @@ -54,6 +56,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; @@ -77,6 +80,15 @@ public class Transitions implements RemoteCallable<Transitions> { /** Transition type for launching 2 tasks simultaneously. */ public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 2; + /** Transition type for exiting PIP via the Shell, via pressing the expand button. */ + public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 3; + + /** Transition type for removing PIP via the Shell, either via Dismiss bubble or Close. */ + public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 4; + + /** Transition type for entering split by opening an app into side-stage. */ + public static final int TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE = TRANSIT_FIRST_CUSTOM + 5; + private final WindowOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; @@ -91,27 +103,29 @@ public class Transitions implements RemoteCallable<Transitions> { private float mTransitionAnimationScaleSetting = 1.0f; private static final class ActiveTransition { - IBinder mToken = null; - TransitionHandler mHandler = null; - boolean mMerged = false; - TransitionInfo mInfo = null; - SurfaceControl.Transaction mStartT = null; - SurfaceControl.Transaction mFinishT = null; + IBinder mToken; + TransitionHandler mHandler; + boolean mMerged; + boolean mAborted; + TransitionInfo mInfo; + SurfaceControl.Transaction mStartT; + SurfaceControl.Transaction mFinishT; } /** Keeps track of currently playing transitions in the order of receipt. */ private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>(); public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, - @NonNull Context context, @NonNull ShellExecutor mainExecutor, - @NonNull ShellExecutor animExecutor) { + @NonNull DisplayController displayController, @NonNull Context context, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mPlayerImpl = new TransitionPlayerImpl(); // The very last handler (0 in the list) should be the default one. - mHandlers.add(new DefaultTransitionHandler(pool, context, mainExecutor, animExecutor)); + mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor, + animExecutor)); // Next lowest priority is remote transitions. mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor); mHandlers.add(mRemoteTransitionHandler); @@ -157,28 +171,12 @@ public class Transitions implements RemoteCallable<Transitions> { } } - /** Create an empty/non-registering transitions object for system-ui tests. */ - @VisibleForTesting - public static ShellTransitions createEmptyForTesting() { - return new ShellTransitions() { - @Override - public void registerRemote(@androidx.annotation.NonNull TransitionFilter filter, - @androidx.annotation.NonNull IRemoteTransition remoteTransition) { - // Do nothing - } - - @Override - public void unregisterRemote( - @androidx.annotation.NonNull IRemoteTransition remoteTransition) { - // Do nothing - } - }; - } - /** Register this transition handler with Core */ public void register(ShellTaskOrganizer taskOrganizer) { if (mPlayerImpl == null) return; taskOrganizer.registerTransitionPlayer(mPlayerImpl); + // Pre-load the instance. + TransitionMetrics.getInstance(); } /** @@ -205,12 +203,12 @@ public class Transitions implements RemoteCallable<Transitions> { /** Register a remote transition to be used when `filter` matches an incoming transition */ public void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition) { + @NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.addFiltered(filter, remoteTransition); } /** Unregisters a remote transition and all associated filters */ - public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.removeFiltered(remoteTransition); } @@ -218,7 +216,7 @@ public class Transitions implements RemoteCallable<Transitions> { public static boolean isOpeningType(@WindowManager.TransitionType int type) { return type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT - || type == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; + || type == TRANSIT_KEYGUARD_GOING_AWAY; } /** @return true if the transition was triggered by closing something vs opening something */ @@ -382,7 +380,7 @@ public class Transitions implements RemoteCallable<Transitions> { } boolean startAnimation(@NonNull ActiveTransition active, TransitionHandler handler) { - return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, + return handler.startAnimation(active.mToken, active.mInfo, active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active.mToken, wct, cb)); } @@ -416,17 +414,19 @@ public class Transitions implements RemoteCallable<Transitions> { /** Special version of finish just for dealing with no-op/invalid transitions. */ private void onAbort(IBinder transition) { - final int activeIdx = findActiveTransition(transition); - if (activeIdx < 0) return; - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Transition animation aborted due to no-op, notifying core %s", transition); - mActiveTransitions.remove(activeIdx); - mOrganizer.finishTransition(transition, null /* wct */, null /* wctCB */); + onFinish(transition, null /* wct */, null /* wctCB */, true /* abort */); } private void onFinish(IBinder transition, @Nullable WindowContainerTransaction wct, @Nullable WindowContainerTransactionCallback wctCB) { + onFinish(transition, wct, wctCB, false /* abort */); + } + + private void onFinish(IBinder transition, + @Nullable WindowContainerTransaction wct, + @Nullable WindowContainerTransactionCallback wctCB, + boolean abort) { int activeIdx = findActiveTransition(transition); if (activeIdx < 0) { Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or " @@ -434,28 +434,37 @@ public class Transitions implements RemoteCallable<Transitions> { return; } else if (activeIdx > 0) { // This transition was merged. - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s", - transition); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged (abort=%b:" + + " %s", abort, transition); final ActiveTransition active = mActiveTransitions.get(activeIdx); active.mMerged = true; + active.mAborted = abort; if (active.mHandler != null) { active.mHandler.onTransitionMerged(active.mToken); } return; } + mActiveTransitions.get(activeIdx).mAborted = abort; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, - "Transition animation finished, notifying core %s", transition); + "Transition animation finished (abort=%b), notifying core %s", abort, transition); // Merge all relevant transactions together SurfaceControl.Transaction fullFinish = mActiveTransitions.get(activeIdx).mFinishT; for (int iA = activeIdx + 1; iA < mActiveTransitions.size(); ++iA) { final ActiveTransition toMerge = mActiveTransitions.get(iA); if (!toMerge.mMerged) break; + // aborted transitions have no start/finish transactions + if (mActiveTransitions.get(iA).mStartT == null) break; + if (fullFinish == null) { + fullFinish = new SurfaceControl.Transaction(); + } // Include start. It will be a no-op if it was already applied. Otherwise, we need it // to maintain consistent state. fullFinish.merge(mActiveTransitions.get(iA).mStartT); fullFinish.merge(mActiveTransitions.get(iA).mFinishT); } - fullFinish.apply(); + if (fullFinish != null) { + fullFinish.apply(); + } // Now perform all the finishes. mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(transition, wct, wctCB); @@ -464,6 +473,12 @@ public class Transitions implements RemoteCallable<Transitions> { ActiveTransition merged = mActiveTransitions.remove(activeIdx); mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */); } + // sift through aborted transitions + while (mActiveTransitions.size() > activeIdx + && mActiveTransitions.get(activeIdx).mAborted) { + ActiveTransition aborted = mActiveTransitions.remove(activeIdx); + mOrganizer.finishTransition(aborted.mToken, null /* wct */, null /* wctCB */); + } if (mActiveTransitions.size() <= activeIdx) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition animations " + "finished"); @@ -494,6 +509,12 @@ public class Transitions implements RemoteCallable<Transitions> { int mergeIdx = activeIdx + 1; while (mergeIdx < mActiveTransitions.size()) { ActiveTransition mergeCandidate = mActiveTransitions.get(mergeIdx); + if (mergeCandidate.mAborted) { + // transition was aborted, so we can skip for now (still leave it in the list + // so that it gets cleaned-up in the right order). + ++mergeIdx; + continue; + } if (mergeCandidate.mMerged) { throw new IllegalStateException("Can't merge a transition after not-merging" + " a preceding one."); @@ -566,12 +587,19 @@ public class Transitions implements RemoteCallable<Transitions> { * Starts a transition animation. This is always called if handleRequest returned non-null * for a particular transition. Otherwise, it is only called if no other handler before * it handled the transition. - * + * @param startTransaction the transaction given to the handler to be applied before the + * transition animation. Note the handler is expected to call on + * {@link SurfaceControl.Transaction#apply()} for startTransaction. + * @param finishTransaction the transaction given to the handler to be applied after the + * transition animation. Unlike startTransaction, the handler is NOT + * expected to apply this transaction. The Transition system will + * apply it when finishCallback is called. * @param finishCallback Call this when finished. This MUST be called on main thread. * @return true if transition was handled, false if not (falls-back to default). */ boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull TransitionFinishCallback finishCallback); /** @@ -661,14 +689,14 @@ public class Transitions implements RemoteCallable<Transitions> { @Override public void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition) { + @NonNull RemoteTransition remoteTransition) { mMainExecutor.execute(() -> { mRemoteTransitionHandler.addFiltered(filter, remoteTransition); }); } @Override - public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { mMainExecutor.execute(() -> { mRemoteTransitionHandler.removeFiltered(remoteTransition); }); @@ -695,7 +723,7 @@ public class Transitions implements RemoteCallable<Transitions> { @Override public void registerRemote(@NonNull TransitionFilter filter, - @NonNull IRemoteTransition remoteTransition) { + @NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "registerRemote", (transitions) -> { transitions.mRemoteTransitionHandler.addFiltered(filter, remoteTransition); @@ -703,7 +731,7 @@ public class Transitions implements RemoteCallable<Transitions> { } @Override - public void unregisterRemote(@NonNull IRemoteTransition remoteTransition) { + public void unregisterRemote(@NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "unregisterRemote", (transitions) -> { transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java new file mode 100644 index 000000000000..2c668ed3d84d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 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.transition; + +import android.graphics.ColorSpace; +import android.graphics.GraphicBuffer; +import android.graphics.PixelFormat; +import android.hardware.HardwareBuffer; +import android.view.SurfaceControl; +import android.view.SurfaceSession; + +/** + * Represents a surface that is displayed over a transition surface. + */ +class WindowThumbnail { + + private SurfaceControl mSurfaceControl; + + private WindowThumbnail() {} + + /** Create a thumbnail surface and attach it over a parent surface. */ + static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent, + HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) { + WindowThumbnail windowThumbnail = new WindowThumbnail(); + windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession) + .setParent(parent) + .setName("WindowThumanil : " + parent.toString()) + .setCallsite("WindowThumanil") + .setFormat(PixelFormat.TRANSLUCENT) + .build(); + + GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(thumbnailHeader); + t.setBuffer(windowThumbnail.mSurfaceControl, graphicBuffer); + t.setColorSpace(windowThumbnail.mSurfaceControl, ColorSpace.get(ColorSpace.Named.SRGB)); + t.setLayer(windowThumbnail.mSurfaceControl, Integer.MAX_VALUE); + t.show(windowThumbnail.mSurfaceControl); + t.apply(); + + return windowThumbnail; + } + + SurfaceControl getSurface() { + return mSurfaceControl; + } + + /** Remove the thumbnail surface and release the surface. */ + void destroy(SurfaceControl.Transaction t) { + if (mSurfaceControl == null) { + return; + } + + t.remove(mSurfaceControl); + t.apply(); + mSurfaceControl.release(); + mSurfaceControl = null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java new file mode 100644 index 000000000000..367676f54aba --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/ShellUnfoldProgressProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 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.unfold; + +import android.annotation.FloatRange; + +import java.util.concurrent.Executor; + +/** + * Wrapper interface for unfold transition progress provider for the Shell + * @see com.android.systemui.unfold.UnfoldTransitionProgressProvider + */ +public interface ShellUnfoldProgressProvider { + + // This is a temporary workaround until we move the progress providers into the Shell or + // refactor the dependencies. TLDR, the base module depends on this provider to determine if the + // FullscreenUnfoldController is available, but this check can't rely on an optional component. + public static final ShellUnfoldProgressProvider NO_PROVIDER = + new ShellUnfoldProgressProvider() {}; + + /** + * Adds a transition listener + */ + default void addListener(Executor executor, UnfoldListener listener) {} + + /** + * Listener for receiving unfold updates + */ + interface UnfoldListener { + default void onStateChangeStarted() {} + + default void onStateChangeProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {} + + default void onStateChangeFinished() {} + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java new file mode 100644 index 000000000000..9faf454261d3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldBackgroundController.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 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.unfold; + +import static android.graphics.Color.blue; +import static android.graphics.Color.green; +import static android.graphics.Color.red; +import static android.view.Display.DEFAULT_DISPLAY; + +import android.annotation.NonNull; +import android.content.Context; +import android.view.SurfaceControl; + +import com.android.wm.shell.R; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; + +/** + * Controls background color layer for the unfold animations + */ +public class UnfoldBackgroundController { + + private static final int BACKGROUND_LAYER_Z_INDEX = -1; + + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final float[] mBackgroundColor; + private SurfaceControl mBackgroundLayer; + + public UnfoldBackgroundController( + @NonNull Context context, + @NonNull RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mBackgroundColor = getBackgroundColor(context); + } + + /** + * Ensures that unfold animation background color layer is present, + * @param transaction where we should add the background if it is not added + */ + public void ensureBackground(@NonNull SurfaceControl.Transaction transaction) { + if (mBackgroundLayer != null) return; + + SurfaceControl.Builder colorLayerBuilder = new SurfaceControl.Builder() + .setName("app-unfold-background") + .setCallsite("AppUnfoldTransitionController") + .setColorLayer(); + mRootTaskDisplayAreaOrganizer.attachToDisplayArea(DEFAULT_DISPLAY, colorLayerBuilder); + mBackgroundLayer = colorLayerBuilder.build(); + + transaction + .setColor(mBackgroundLayer, mBackgroundColor) + .show(mBackgroundLayer) + .setLayer(mBackgroundLayer, BACKGROUND_LAYER_Z_INDEX); + } + + /** + * Ensures that the background is not visible + * @param transaction as part of which the removal will happen if needed + */ + public void removeBackground(@NonNull SurfaceControl.Transaction transaction) { + if (mBackgroundLayer == null) return; + if (mBackgroundLayer.isValid()) { + transaction.remove(mBackgroundLayer); + } + mBackgroundLayer = null; + } + + private float[] getBackgroundColor(Context context) { + int colorInt = context.getResources().getColor(R.color.unfold_transition_background); + return new float[]{ + (float) red(colorInt) / 255.0F, + (float) green(colorInt) / 255.0F, + (float) blue(colorInt) / 255.0F + }; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java new file mode 100644 index 000000000000..b9b671635010 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/CounterRotator.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 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.util; + +import android.view.SurfaceControl; + +import java.util.ArrayList; + +/** + * Utility class that takes care of counter-rotating surfaces during a transition animation. + */ +public class CounterRotator { + SurfaceControl mSurface = null; + ArrayList<SurfaceControl> mRotateChildren = null; + + /** Gets the surface with the counter-rotation. */ + public SurfaceControl getSurface() { + return mSurface; + } + + /** + * Sets up this rotator. + * + * @param rotateDelta is the forward rotation change (the rotation the display is making). + * @param displayW (and H) Is the size of the rotating display. + */ + public void setup(SurfaceControl.Transaction t, SurfaceControl parent, int rotateDelta, + float displayW, float displayH) { + if (rotateDelta == 0) return; + mRotateChildren = new ArrayList<>(); + // We want to counter-rotate, so subtract from 4 + rotateDelta = 4 - (rotateDelta + 4) % 4; + mSurface = new SurfaceControl.Builder() + .setName("Transition Unrotate") + .setContainerLayer() + .setParent(parent) + .build(); + // column-major + if (rotateDelta == 1) { + t.setMatrix(mSurface, 0, 1, -1, 0); + t.setPosition(mSurface, displayW, 0); + } else if (rotateDelta == 2) { + t.setMatrix(mSurface, -1, 0, 0, -1); + t.setPosition(mSurface, displayW, displayH); + } else if (rotateDelta == 3) { + t.setMatrix(mSurface, 0, -1, 1, 0); + t.setPosition(mSurface, 0, displayH); + } + t.show(mSurface); + } + + /** + * Add a surface that needs to be counter-rotate. + */ + public void addChild(SurfaceControl.Transaction t, SurfaceControl child) { + if (mSurface == null) return; + t.reparent(child, mSurface); + mRotateChildren.add(child); + } + + /** + * Clean-up. This undoes any reparenting and effectively stops the counter-rotation. + */ + public void cleanUp(SurfaceControl rootLeash) { + if (mSurface == null) return; + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + for (int i = mRotateChildren.size() - 1; i >= 0; --i) { + t.reparent(mRotateChildren.get(i), rootLeash); + } + t.remove(mSurface); + t.apply(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl new file mode 100644 index 000000000000..15797cdb9aba --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 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.util; + +parcelable GroupedRecentTaskInfo;
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java new file mode 100644 index 000000000000..603d05d78fc0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/GroupedRecentTaskInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 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.util; + +import android.app.ActivityManager; +import android.app.WindowConfiguration; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Simple container for recent tasks. May contain either a single or pair of tasks. + */ +public class GroupedRecentTaskInfo implements Parcelable { + public @NonNull ActivityManager.RecentTaskInfo mTaskInfo1; + public @Nullable ActivityManager.RecentTaskInfo mTaskInfo2; + public @Nullable StagedSplitBounds mStagedSplitBounds; + + public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1) { + this(task1, null, null); + } + + public GroupedRecentTaskInfo(@NonNull ActivityManager.RecentTaskInfo task1, + @Nullable ActivityManager.RecentTaskInfo task2, + @Nullable StagedSplitBounds stagedSplitBounds) { + mTaskInfo1 = task1; + mTaskInfo2 = task2; + mStagedSplitBounds = stagedSplitBounds; + } + + GroupedRecentTaskInfo(Parcel parcel) { + mTaskInfo1 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR); + mTaskInfo2 = parcel.readTypedObject(ActivityManager.RecentTaskInfo.CREATOR); + mStagedSplitBounds = parcel.readTypedObject(StagedSplitBounds.CREATOR); + } + + @Override + public String toString() { + String taskString = "Task1: " + getTaskInfo(mTaskInfo1) + + ", Task2: " + getTaskInfo(mTaskInfo2); + if (mStagedSplitBounds != null) { + taskString += ", SplitBounds: " + mStagedSplitBounds.toString(); + } + return taskString; + } + + private String getTaskInfo(ActivityManager.RecentTaskInfo taskInfo) { + if (taskInfo == null) { + return null; + } + return "id=" + taskInfo.taskId + + " baseIntent=" + (taskInfo.baseIntent != null + ? taskInfo.baseIntent.getComponent() + : "null") + + " winMode=" + WindowConfiguration.windowingModeToString( + taskInfo.getWindowingMode()); + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeTypedObject(mTaskInfo1, flags); + parcel.writeTypedObject(mTaskInfo2, flags); + parcel.writeTypedObject(mStagedSplitBounds, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @android.annotation.NonNull Creator<GroupedRecentTaskInfo> CREATOR = + new Creator<GroupedRecentTaskInfo>() { + public GroupedRecentTaskInfo createFromParcel(Parcel source) { + return new GroupedRecentTaskInfo(source); + } + public GroupedRecentTaskInfo[] newArray(int size) { + return new GroupedRecentTaskInfo[size]; + } + }; +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java b/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java new file mode 100644 index 000000000000..a0c84cc33ebd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/StagedSplitBounds.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 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.util; + +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Container of various information needed to display split screen + * tasks/leashes/etc in Launcher + */ +public class StagedSplitBounds implements Parcelable { + public final Rect leftTopBounds; + public final Rect rightBottomBounds; + /** This rect represents the actual gap between the two apps */ + public final Rect visualDividerBounds; + // This class is orientation-agnostic, so we compute both for later use + public final float topTaskPercent; + public final float leftTaskPercent; + /** + * If {@code true}, that means at the time of creation of this object, the + * split-screened apps were vertically stacked. This is useful in scenarios like + * rotation where the bounds won't change, but this variable can indicate what orientation + * the bounds were originally in + */ + public final boolean appsStackedVertically; + public final int leftTopTaskId; + public final int rightBottomTaskId; + + public StagedSplitBounds(Rect leftTopBounds, Rect rightBottomBounds, + int leftTopTaskId, int rightBottomTaskId) { + this.leftTopBounds = leftTopBounds; + this.rightBottomBounds = rightBottomBounds; + this.leftTopTaskId = leftTopTaskId; + this.rightBottomTaskId = rightBottomTaskId; + + if (rightBottomBounds.top > leftTopBounds.top) { + // vertical apps, horizontal divider + this.visualDividerBounds = new Rect(leftTopBounds.left, leftTopBounds.bottom, + leftTopBounds.right, rightBottomBounds.top); + appsStackedVertically = true; + } else { + // horizontal apps, vertical divider + this.visualDividerBounds = new Rect(leftTopBounds.right, leftTopBounds.top, + rightBottomBounds.left, leftTopBounds.bottom); + appsStackedVertically = false; + } + + leftTaskPercent = this.leftTopBounds.width() / (float) rightBottomBounds.right; + topTaskPercent = this.leftTopBounds.height() / (float) rightBottomBounds.bottom; + } + + public StagedSplitBounds(Parcel parcel) { + leftTopBounds = parcel.readTypedObject(Rect.CREATOR); + rightBottomBounds = parcel.readTypedObject(Rect.CREATOR); + visualDividerBounds = parcel.readTypedObject(Rect.CREATOR); + topTaskPercent = parcel.readFloat(); + leftTaskPercent = parcel.readFloat(); + appsStackedVertically = parcel.readBoolean(); + leftTopTaskId = parcel.readInt(); + rightBottomTaskId = parcel.readInt(); + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeTypedObject(leftTopBounds, flags); + parcel.writeTypedObject(rightBottomBounds, flags); + parcel.writeTypedObject(visualDividerBounds, flags); + parcel.writeFloat(topTaskPercent); + parcel.writeFloat(leftTaskPercent); + parcel.writeBoolean(appsStackedVertically); + parcel.writeInt(leftTopTaskId); + parcel.writeInt(rightBottomTaskId); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StagedSplitBounds)) { + return false; + } + // Only need to check the base fields (the other fields are derived from these) + final StagedSplitBounds other = (StagedSplitBounds) obj; + return Objects.equals(leftTopBounds, other.leftTopBounds) + && Objects.equals(rightBottomBounds, other.rightBottomBounds) + && leftTopTaskId == other.leftTopTaskId + && rightBottomTaskId == other.rightBottomTaskId; + } + + @Override + public int hashCode() { + return Objects.hash(leftTopBounds, rightBottomBounds, leftTopTaskId, rightBottomTaskId); + } + + @Override + public String toString() { + return "LeftTop: " + leftTopBounds + ", taskId: " + leftTopTaskId + "\n" + + "RightBottom: " + rightBottomBounds + ", taskId: " + rightBottomTaskId + "\n" + + "Divider: " + visualDividerBounds + "\n" + + "AppsVertical? " + appsStackedVertically; + } + + public static final Creator<StagedSplitBounds> CREATOR = new Creator<StagedSplitBounds>() { + @Override + public StagedSplitBounds createFromParcel(Parcel in) { + return new StagedSplitBounds(in); + } + + @Override + public StagedSplitBounds[] newArray(int size) { + return new StagedSplitBounds[size]; + } + }; +} diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index 9dd25fe0e6fe..3ca5b9c38aff 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -25,11 +25,17 @@ package { android_test { name: "WMShellFlickerTests", - srcs: ["src/**/*.java", "src/**/*.kt"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], manifest: "AndroidManifest.xml", test_config: "AndroidTest.xml", platform_apis: true, certificate: "platform", + optimize: { + enabled: false, + }, test_suites: ["device-tests"], libs: ["android.test.runner"], static_libs: [ diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml index e6d32ff1166f..06df9568e01a 100644 --- a/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/AndroidManifest.xml @@ -42,6 +42,9 @@ <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> <!-- ATM.removeRootTasksWithActivityTypes() --> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Enable bubble notification--> + <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> + <!-- Allow the test to write directly to /sdcard/ --> <application android:requestLegacyExternalStorage="true"> <uses-library android:name="android.test.runner"/> diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt index c5b5b91d570b..c4be785cff19 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt @@ -14,100 +14,104 @@ * limitations under the License. */ +@file:JvmName("CommonAssertions") package com.android.wm.shell.flicker import android.graphics.Region import android.view.Surface import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.traces.layers.getVisibleBounds +import com.android.server.wm.traces.common.FlickerComponentName -fun FlickerTestParameter.appPairsDividerIsVisible() { +fun FlickerTestParameter.appPairsDividerIsVisibleAtEnd() { assertLayersEnd { - this.isVisible(APP_PAIR_SPLIT_DIVIDER) + this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsDividerIsInvisible() { +fun FlickerTestParameter.appPairsDividerIsInvisibleAtEnd() { assertLayersEnd { - this.notContains(APP_PAIR_SPLIT_DIVIDER) + this.notContains(APP_PAIR_SPLIT_DIVIDER_COMPONENT) } } fun FlickerTestParameter.appPairsDividerBecomesVisible() { assertLayers { - this.isInvisible(DOCKED_STACK_DIVIDER) + this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() - .isVisible(DOCKED_STACK_DIVIDER) + .isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.dockedStackDividerIsVisible() { +fun FlickerTestParameter.dockedStackDividerIsVisibleAtEnd() { assertLayersEnd { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } fun FlickerTestParameter.dockedStackDividerBecomesVisible() { assertLayers { - this.isInvisible(DOCKED_STACK_DIVIDER) + this.isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() - .isVisible(DOCKED_STACK_DIVIDER) + .isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } fun FlickerTestParameter.dockedStackDividerBecomesInvisible() { assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) .then() - .isInvisible(DOCKED_STACK_DIVIDER) + .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.dockedStackDividerIsInvisible() { +fun FlickerTestParameter.dockedStackDividerNotExistsAtEnd() { assertLayersEnd { - this.notContains(DOCKED_STACK_DIVIDER) + this.notContains(DOCKED_STACK_DIVIDER_COMPONENT) } } -fun FlickerTestParameter.appPairsPrimaryBoundsIsVisible(rotation: Int, primaryLayerName: String) { +fun FlickerTestParameter.appPairsPrimaryBoundsIsVisibleAtEnd( + rotation: Int, + primaryComponent: FlickerComponentName +) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(primaryLayerName) - .coversExactly(getPrimaryRegion(dividerRegion, rotation)) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryComponent) + .overlaps(getPrimaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisible( +fun FlickerTestParameter.dockedStackPrimaryBoundsIsVisibleAtEnd( rotation: Int, - primaryLayerName: String + primaryComponent: FlickerComponentName ) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) - visibleRegion(primaryLayerName) - .coversExactly(getPrimaryRegion(dividerRegion, rotation)) + val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryComponent) + .overlaps(getPrimaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.appPairsSecondaryBoundsIsVisible( +fun FlickerTestParameter.appPairsSecondaryBoundsIsVisibleAtEnd( rotation: Int, - secondaryLayerName: String + secondaryComponent: FlickerComponentName ) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(secondaryLayerName) - .coversExactly(getSecondaryRegion(dividerRegion, rotation)) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(secondaryComponent) + .overlaps(getSecondaryRegion(dividerRegion, rotation)) } } -fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisible( +fun FlickerTestParameter.dockedStackSecondaryBoundsIsVisibleAtEnd( rotation: Int, - secondaryLayerName: String + secondaryComponent: FlickerComponentName ) { assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(DOCKED_STACK_DIVIDER) - visibleRegion(secondaryLayerName) - .coversExactly(getSecondaryRegion(dividerRegion, rotation)) + val dividerRegion = layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(secondaryComponent) + .overlaps(getSecondaryRegion(dividerRegion, rotation)) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt index 03b93c74233c..40891f36a5da 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt @@ -14,9 +14,11 @@ * limitations under the License. */ +@file:JvmName("CommonConstants") package com.android.wm.shell.flicker -const val IME_WINDOW_NAME = "InputMethod" +import com.android.server.wm.traces.common.FlickerComponentName + const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" -const val APP_PAIR_SPLIT_DIVIDER = "AppPairSplitDivider" -const val DOCKED_STACK_DIVIDER = "DockedStackDivider"
\ No newline at end of file +val APP_PAIR_SPLIT_DIVIDER_COMPONENT = FlickerComponentName("", "AppPairSplitDivider#") +val DOCKED_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider#")
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt index a6d67355f271..b63d9fffdb61 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:JvmName("WaitUtils") package com.android.wm.shell.flicker import android.os.SystemClock diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt index ef9f7421fd60..038be9c190c2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestCannotPairNonResizeableApps.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -25,7 +24,7 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.appPairsDividerIsInvisible +import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow @@ -61,7 +60,7 @@ class AppPairsTestCannotPairNonResizeableApps( // TODO pair apps through normal UX flow executeShellCommand( composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) } } } @@ -85,15 +84,13 @@ class AppPairsTestCannotPairNonResizeableApps( @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @FlakyTest + @Presubmit @Test - override fun navBarLayerIsAlwaysVisible() { - super.navBarLayerIsAlwaysVisible() - } + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() @Presubmit @Test - fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible() + fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() @Presubmit @Test @@ -103,8 +100,8 @@ class AppPairsTestCannotPairNonResizeableApps( "Non resizeable app not initialized" } testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) - isInvisible(primaryApp.defaultWindowName) + isAppWindowVisible(nonResizeableApp.component) + isAppWindowInvisible(primaryApp.component) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt index db63c4c43523..bbc6b2dbece8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestPairPrimaryAndSecondaryApps.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -25,10 +24,10 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER -import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -54,10 +53,14 @@ class AppPairsTestPairPrimaryAndSecondaryApps( // TODO pair apps through normal UX flow executeShellCommand( composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + waitAppsShown(primaryApp, secondaryApp) } } + @Presubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + @FlakyTest @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @@ -68,14 +71,14 @@ class AppPairsTestPairPrimaryAndSecondaryApps( @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(primaryApp.defaultWindowName) - isVisible(secondaryApp.defaultWindowName) + isAppWindowVisible(primaryApp.component) + isAppWindowVisible(secondaryApp.component) } } @@ -83,10 +86,10 @@ class AppPairsTestPairPrimaryAndSecondaryApps( @Test fun appsEndingBounds() { testSpec.assertLayersEnd { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(primaryApp.defaultWindowName) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryApp.component) .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion)) - visibleRegion(secondaryApp.defaultWindowName) + visibleRegion(secondaryApp.component) .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion)) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt index c8d34237231c..bb784a809b7e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestSupportPairNonResizeableApps.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -25,7 +24,7 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.appPairsDividerIsVisible +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow @@ -61,7 +60,7 @@ class AppPairsTestSupportPairNonResizeableApps( // TODO pair apps through normal UX flow executeShellCommand( composePairsCommand(primaryTaskId, nonResizeableTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + nonResizeableApp?.run { wmHelper.waitForFullScreenApp(nonResizeableApp.component) } } } @@ -77,6 +76,10 @@ class AppPairsTestSupportPairNonResizeableApps( resetMultiWindowConfig(instrumentation) } + @Presubmit + @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + @FlakyTest @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @@ -87,7 +90,7 @@ class AppPairsTestSupportPairNonResizeableApps( @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() @Presubmit @Test @@ -97,8 +100,8 @@ class AppPairsTestSupportPairNonResizeableApps( "Non resizeable app not initialized" } testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) - isVisible(primaryApp.defaultWindowName) + isAppWindowVisible(nonResizeableApp.component) + isAppWindowVisible(primaryApp.component) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt index 83df83600d11..a1a4db112dfd 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTestUnpairPrimaryAndSecondaryApps.kt @@ -25,10 +25,10 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER -import com.android.wm.shell.flicker.appPairsDividerIsInvisible +import com.android.wm.shell.flicker.APP_PAIR_SPLIT_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.appPairsDividerIsInvisibleAtEnd import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -51,9 +51,11 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( get() = { super.transition(this, it) setup { - executeShellCommand( - composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + eachRun { + executeShellCommand( + composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) + waitAppsShown(primaryApp, secondaryApp) + } } transitions { // TODO pair apps through normal UX flow @@ -73,14 +75,14 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Presubmit @Test - fun appPairsDividerIsInvisible() = testSpec.appPairsDividerIsInvisible() + fun appPairsDividerIsInvisibleAtEnd() = testSpec.appPairsDividerIsInvisibleAtEnd() @Presubmit @Test fun bothAppWindowsInvisible() { testSpec.assertWmEnd { - isInvisible(primaryApp.defaultWindowName) - isInvisible(secondaryApp.defaultWindowName) + isAppWindowInvisible(primaryApp.component) + isAppWindowInvisible(secondaryApp.component) } } @@ -88,10 +90,10 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Test fun appsStartingBounds() { testSpec.assertLayersStart { - val dividerRegion = entry.getVisibleBounds(APP_PAIR_SPLIT_DIVIDER) - visibleRegion(primaryApp.defaultWindowName) + val dividerRegion = layer(APP_PAIR_SPLIT_DIVIDER_COMPONENT).visibleRegion.region + visibleRegion(primaryApp.component) .coversExactly(appPairsHelper.getPrimaryBounds(dividerRegion)) - visibleRegion(secondaryApp.defaultWindowName) + visibleRegion(secondaryApp.component) .coversExactly(appPairsHelper.getSecondaryBounds(dividerRegion)) } } @@ -100,16 +102,14 @@ class AppPairsTestUnpairPrimaryAndSecondaryApps( @Test fun appsEndingBounds() { testSpec.assertLayersEnd { - notContains(primaryApp.defaultWindowName) - notContains(secondaryApp.defaultWindowName) + notContains(primaryApp.component) + notContains(secondaryApp.component) } } - @FlakyTest + @Presubmit @Test - override fun navBarLayerIsAlwaysVisible() { - super.navBarLayerIsAlwaysVisible() - } + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt index 1935bb97849c..9e20bbbc1a1b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/AppPairsTransition.kt @@ -20,24 +20,23 @@ import android.app.Instrumentation import android.content.Context import android.platform.test.annotations.Presubmit import android.system.helpers.ActivityHelper -import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.isRotated import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.helpers.AppPairsHelper import com.android.wm.shell.flicker.helpers.BaseAppHelper import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow @@ -55,7 +54,7 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) protected val activityHelper = ActivityHelper.getInstance() protected val appPairsHelper = AppPairsHelper(instrumentation, Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT) + Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) protected val primaryApp = SplitScreenHelper.getPrimary(instrumentation) protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) @@ -154,39 +153,33 @@ abstract class AppPairsTransition(protected val testSpec: FlickerTestParameter) @FlakyTest(bugId = 186510496) @Test - open fun navBarLayerIsAlwaysVisible() { - testSpec.navBarLayerIsAlwaysVisible() + open fun navBarLayerIsVisible() { + testSpec.navBarLayerIsVisible() } @Presubmit @Test - open fun statusBarLayerIsAlwaysVisible() { - testSpec.statusBarLayerIsAlwaysVisible() + open fun statusBarLayerIsVisible() { + testSpec.statusBarLayerIsVisible() } @Presubmit @Test - open fun navBarWindowIsAlwaysVisible() { - testSpec.navBarWindowIsAlwaysVisible() + open fun navBarWindowIsVisible() { + testSpec.navBarWindowIsVisible() } @Presubmit @Test - open fun statusBarWindowIsAlwaysVisible() { - testSpec.statusBarWindowIsAlwaysVisible() + open fun statusBarWindowIsVisible() { + testSpec.statusBarWindowIsVisible() } @Presubmit @Test - open fun navBarLayerRotatesAndScales() { - testSpec.navBarLayerRotatesAndScales(Surface.ROTATION_0, - testSpec.config.endRotation) - } + open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Presubmit @Test - open fun statusBarLayerRotatesScales() { - testSpec.statusBarLayerRotatesScales(Surface.ROTATION_0, - testSpec.config.endRotation) - } + open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt index c875c0006703..56a2531a3fe1 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsInAppPairsMode.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest @@ -28,10 +27,10 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.setRotation -import com.android.wm.shell.flicker.appPairsDividerIsVisible -import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible -import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -57,41 +56,43 @@ class RotateTwoLaunchedAppsInAppPairsMode( transitions { executeShellCommand(composePairsCommand( primaryTaskId, secondaryTaskId, true /* pair */)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + waitAppsShown(primaryApp, secondaryApp) setRotation(testSpec.config.endRotation) } } - @FlakyTest + @Presubmit @Test - override fun statusBarLayerIsAlwaysVisible() { - super.statusBarLayerIsAlwaysVisible() - } + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + @Presubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @Presubmit @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(primaryApp.defaultWindowName) - .isVisible(secondaryApp.defaultWindowName) + isAppWindowVisible(primaryApp.component) + isAppWindowVisible(secondaryApp.component) } } @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() - @FlakyTest(bugId = 172776659) + @Presubmit @Test - fun appPairsPrimaryBoundsIsVisible() = - testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation, - primaryApp.defaultWindowName) + fun appPairsPrimaryBoundsIsVisibleAtEnd() = + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + primaryApp.component) - @FlakyTest(bugId = 172776659) + @FlakyTest @Test - fun appPairsSecondaryBoundsIsVisible() = - testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation, - secondaryApp.defaultWindowName) + fun appPairsSecondaryBoundsIsVisibleAtEnd() = + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + secondaryApp.component) companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt index c3360ca0f7d3..0699a4fd0512 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsRotateAndEnterAppPairsMode.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.apppairs -import android.os.SystemClock import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest @@ -28,12 +27,10 @@ import com.android.server.wm.flicker.annotation.Group1 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.appPairsDividerIsVisible -import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisible -import com.android.wm.shell.flicker.helpers.AppPairsHelper +import com.android.wm.shell.flicker.appPairsDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.appPairsSecondaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.helpers.AppPairsHelper.Companion.waitAppsShown import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -60,48 +57,50 @@ class RotateTwoLaunchedAppsRotateAndEnterAppPairsMode( this.setRotation(testSpec.config.endRotation) executeShellCommand( composePairsCommand(primaryTaskId, secondaryTaskId, pair = true)) - SystemClock.sleep(AppPairsHelper.TIMEOUT_MS) + waitAppsShown(primaryApp, secondaryApp) } } @Presubmit @Test - fun appPairsDividerIsVisible() = testSpec.appPairsDividerIsVisible() + fun appPairsDividerIsVisibleAtEnd() = testSpec.appPairsDividerIsVisibleAtEnd() @Presubmit @Test - override fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() @Presubmit @Test - override fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() - @FlakyTest + @Presubmit @Test - override fun statusBarLayerIsAlwaysVisible() { - super.statusBarLayerIsAlwaysVisible() - } + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + @Presubmit + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @Presubmit @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(primaryApp.defaultWindowName) - isVisible(secondaryApp.defaultWindowName) + isAppWindowVisible(primaryApp.component) + isAppWindowVisible(secondaryApp.component) } } @FlakyTest(bugId = 172776659) @Test - fun appPairsPrimaryBoundsIsVisible() = - testSpec.appPairsPrimaryBoundsIsVisible(testSpec.config.endRotation, - primaryApp.defaultWindowName) + fun appPairsPrimaryBoundsIsVisibleAtEnd() = + testSpec.appPairsPrimaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + primaryApp.component) @FlakyTest(bugId = 172776659) @Test - fun appPairsSecondaryBoundsIsVisible() = - testSpec.appPairsSecondaryBoundsIsVisible(testSpec.config.endRotation, - secondaryApp.defaultWindowName) + fun appPairsSecondaryBoundsIsVisibleAtEnd() = + testSpec.appPairsSecondaryBoundsIsVisibleAtEnd(testSpec.config.endRotation, + secondaryApp.component) companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt index 512fd9a58ea8..b95193a17265 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/apppairs/RotateTwoLaunchedAppsTransition.kt @@ -22,7 +22,10 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled import com.android.wm.shell.flicker.helpers.SplitScreenHelper +import org.junit.Assume.assumeFalse +import org.junit.Before import org.junit.Test abstract class RotateTwoLaunchedAppsTransition( @@ -37,8 +40,8 @@ abstract class RotateTwoLaunchedAppsTransition( test { device.wakeUpAndGoToHomeScreen() this.setRotation(Surface.ROTATION_0) - primaryApp.launchViaIntent() - secondaryApp.launchViaIntent() + primaryApp.launchViaIntent(wmHelper) + secondaryApp.launchViaIntent(wmHelper) updateTasksId() } } @@ -52,10 +55,17 @@ abstract class RotateTwoLaunchedAppsTransition( } } + @Before + override fun setup() { + // AppPairs hasn't been updated to Shell Transition. There will be conflict on rotation. + assumeFalse(isShellTransitionsEnabled()) + super.setup() + } + @FlakyTest @Test - override fun navBarLayerIsAlwaysVisible() { - super.navBarLayerIsAlwaysVisible() + override fun navBarLayerIsVisible() { + super.navBarLayerIsVisible() } @FlakyTest diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt new file mode 100644 index 000000000000..322d8b5e4dac --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/BaseBubbleScreen.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 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.bubble + +import android.app.INotificationManager +import android.app.Instrumentation +import android.app.NotificationManager +import android.content.Context +import android.os.ServiceManager +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerBuilderProvider +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE +import com.android.server.wm.flicker.repetitions +import com.android.wm.shell.flicker.helpers.LaunchBubbleHelper +import org.junit.Test +import org.junit.runners.Parameterized + +/** + * Base configurations for Bubble flicker tests + */ +abstract class BaseBubbleScreen(protected val testSpec: FlickerTestParameter) { + + protected val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + protected val context: Context = instrumentation.context + protected val testApp = LaunchBubbleHelper(instrumentation) + + protected val notifyManager = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)) + + protected val packageManager = context.getPackageManager() + protected val uid = packageManager.getApplicationInfo( + testApp.component.packageName, 0).uid + + protected lateinit var addBubbleBtn: UiObject2 + protected lateinit var cancelAllBtn: UiObject2 + + protected abstract val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + + @JvmOverloads + protected open fun buildTransition( + extraSpec: FlickerBuilder.(Map<String, Any?>) -> Unit = {} + ): FlickerBuilder.(Map<String, Any?>) -> Unit { + return { configuration -> + + setup { + test { + notifyManager.setBubblesAllowed(testApp.component.packageName, + uid, NotificationManager.BUBBLE_PREFERENCE_ALL) + testApp.launchViaIntent(wmHelper) + addBubbleBtn = device.wait(Until.findObject( + By.text("Add Bubble")), FIND_OBJECT_TIMEOUT) + cancelAllBtn = device.wait(Until.findObject( + By.text("Cancel All Bubble")), FIND_OBJECT_TIMEOUT) + } + } + + teardown { + notifyManager.setBubblesAllowed(testApp.component.packageName, + uid, NotificationManager.BUBBLE_PREFERENCE_NONE) + testApp.exit() + } + + extraSpec(this, configuration) + } + } + + @FlakyTest + @Test + fun testAppIsAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + } + } + + @FlickerBuilderProvider + fun buildFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + repeat { testSpec.config.repetitions } + transition(this, testSpec.config) + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) + } + + const val FIND_OBJECT_TIMEOUT = 2000L + const val SYSTEM_UI_PACKAGE = SYSTEMUI_PACKAGE + const val BUBBLE_RES_NAME = "bubble_view" + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt new file mode 100644 index 000000000000..bfdcb363a818 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/DismissBubbleScreen.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 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.bubble + +import android.content.Context +import android.graphics.Point +import android.util.DisplayMetrics +import android.view.WindowManager +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:DismissBubbleScreen` + * + * Actions: + * Dismiss a bubble notification + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 +class DismissBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val displaySize = DisplayMetrics() + + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition() { + setup { + eachRun { + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found") + } + } + transitions { + wm?.run { wm.getDefaultDisplay().getMetrics(displaySize) } ?: error("WM not found") + val dist = Point((displaySize.widthPixels / 2), displaySize.heightPixels) + val showBubble = device.wait(Until.findObject( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + showBubble?.run { drag(dist, 1000) } ?: error("Show bubble not found") + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt new file mode 100644 index 000000000000..42eeadf3ddd9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ExpandBubbleScreen.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 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.bubble + +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen` + * + * Actions: + * Launch an app and enable app's bubble notification + * Send a bubble notification + * The activity for the bubble is launched + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 +class ExpandBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition() { + setup { + test { + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found") + } + } + transitions { + val showBubble = device.wait(Until.findObject( + By.res("com.android.systemui", "bubble_view")), FIND_OBJECT_TIMEOUT) + showBubble?.run { showBubble.click() } ?: error("Bubble notify not found") + device.pressBack() + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt index cf84a2c696d0..47e8c0c047a8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithDismissButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2021 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. @@ -14,47 +14,35 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.pip +package com.android.wm.shell.flicker.bubble -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder -import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipCloseWithDismissButton` + * Test creating a bubble notification + * + * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen` + * + * Actions: + * Launch an app and enable app's bubble notification + * Send a bubble notification */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class PipCloseWithDismissButtonTest(testSpec: FlickerTestParameter) : PipCloseTransition(testSpec) { +@Group4 +class LaunchBubbleScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { - super.transition(this, it) + get() = buildTransition() { transitions { - pipApp.closePipWindow(wmHelper) + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Bubble widget not found") } } - - @FlakyTest - @Test - override fun pipLayerBecomesInvisible() { - super.pipLayerBecomesInvisible() - } - - @FlakyTest - @Test - override fun pipWindowBecomesInvisible() { - super.pipWindowBecomesInvisible() - } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt new file mode 100644 index 000000000000..194e28fd6e8a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/MultiBubblesScreen.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 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.bubble + +import android.os.SystemClock +import androidx.test.filters.RequiresDevice +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.annotation.Group4 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching a new activity from bubble. + * + * To run this test: `atest WMShellFlickerTests:MultiBubblesScreen` + * + * Actions: + * Switch in different bubble notifications + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 +class MultiBubblesScreen(testSpec: FlickerTestParameter) : BaseBubbleScreen(testSpec) { + + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition() { + setup { + test { + for (i in 1..3) { + addBubbleBtn?.run { addBubbleBtn.click() } ?: error("Add Bubble not found") + } + val showBubble = device.wait(Until.findObject( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + showBubble?.run { showBubble.click() } ?: error("Show bubble not found") + SystemClock.sleep(1000) + } + } + transitions { + val bubbles = device.wait(Until.findObjects( + By.res(SYSTEM_UI_PACKAGE, BUBBLE_RES_NAME)), FIND_OBJECT_TIMEOUT) + for (entry in bubbles) { + entry?.run { entry.click() } ?: error("Bubble not found") + SystemClock.sleep(1000) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt index 5b8cfb81016a..623055f659b9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/AppPairsHelper.kt @@ -17,14 +17,15 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.graphics.Region +import com.android.server.wm.flicker.Flicker import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.traces.common.FlickerComponentName class AppPairsHelper( instrumentation: Instrumentation, activityLabel: String, - component: ComponentName + component: FlickerComponentName ) : BaseAppHelper(instrumentation, activityLabel, component) { fun getPrimaryBounds(dividerBounds: Region): android.graphics.Region { val primaryAppBounds = Region(0, 0, dividerBounds.bounds.right, @@ -43,5 +44,17 @@ class AppPairsHelper( companion object { const val TEST_REPETITIONS = 1 const val TIMEOUT_MS = 3_000L + + fun Flicker.waitAppsShown(app1: SplitScreenHelper?, app2: SplitScreenHelper?) { + wmHelper.waitFor("primaryAndSecondaryAppsVisible") { dump -> + val primaryAppVisible = app1?.let { + dump.wmState.isWindowSurfaceShown(app1.defaultWindowName) + } ?: false + val secondaryAppVisible = app2?.let { + dump.wmState.isWindowSurfaceShown(app2.defaultWindowName) + } ?: false + primaryAppVisible && secondaryAppVisible + } + } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt index 4fe69ad7fabe..57bc0d580d72 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/BaseAppHelper.kt @@ -17,9 +17,9 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.content.pm.PackageManager.FEATURE_LEANBACK import android.content.pm.PackageManager.FEATURE_LEANBACK_ONLY +import android.os.SystemProperties import android.support.test.launcherhelper.LauncherStrategyFactory import android.util.Log import androidx.test.uiautomator.By @@ -27,13 +27,13 @@ import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.flicker.helpers.StandardAppHelper -import com.android.server.wm.traces.parser.toWindowName +import com.android.server.wm.traces.common.FlickerComponentName import java.io.IOException abstract class BaseAppHelper( instrumentation: Instrumentation, launcherName: String, - component: ComponentName + component: FlickerComponentName ) : StandardAppHelper( instrumentation, launcherName, @@ -60,6 +60,9 @@ abstract class BaseAppHelper( companion object { private const val APP_CLOSE_WAIT_TIME_MS = 3_000L + fun isShellTransitionsEnabled() = + SystemProperties.getBoolean("persist.debug.shell_transit", false) + fun executeShellCommand(instrumentation: Instrumentation, cmd: String) { try { SystemUtil.runShellCommand(instrumentation, cmd) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt index b4ae18749b34..471e010cf560 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/FixedAppHelper.kt @@ -17,10 +17,11 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.testapp.Components class FixedAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.FixedActivity.LABEL, - Components.FixedActivity.COMPONENT + Components.FixedActivity.COMPONENT.toFlickerComponent() )
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt index cac46fe676b3..0f00edea136f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/ImeAppHelper.kt @@ -21,13 +21,14 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.server.wm.flicker.helpers.FIND_TIMEOUT +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.testapp.Components open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.ImeActivity.LABEL, - Components.ImeActivity.COMPONENT + Components.ImeActivity.COMPONENT.toFlickerComponent() ) { /** * Opens the IME and wait for it to be displayed @@ -61,7 +62,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( if (wmHelper == null) { device.waitForIdle() } else { - require(wmHelper.waitImeWindowShown()) { "IME did not appear" } + require(wmHelper.waitImeShown()) { "IME did not appear" } } } @@ -78,7 +79,7 @@ open class ImeAppHelper(instrumentation: Instrumentation) : BaseAppHelper( if (wmHelper == null) { uiDevice.waitForIdle() } else { - require(wmHelper.waitImeWindowGone()) { "IME did did not close" } + require(wmHelper.waitImeGone()) { "IME did did not close" } } } else { // While pressing the back button should close the IME on TV as well, it may also lead diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt new file mode 100644 index 000000000000..6695c17ed514 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/LaunchBubbleHelper.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 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.helpers + +import android.app.Instrumentation +import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.wm.shell.flicker.testapp.Components + +class LaunchBubbleHelper(instrumentation: Instrumentation) : BaseAppHelper( + instrumentation, + Components.LaunchBubbleActivity.LABEL, + Components.LaunchBubbleActivity.COMPONENT.toFlickerComponent() +) { + + companion object { + const val TEST_REPETITIONS = 1 + const val TIMEOUT_MS = 3_000L + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt index 7f99e62b36b0..12ccbafce651 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/MultiWindowHelper.kt @@ -17,14 +17,14 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName import android.content.Context import android.provider.Settings +import com.android.server.wm.traces.common.FlickerComponentName class MultiWindowHelper( instrumentation: Instrumentation, activityLabel: String, - componentsInfo: ComponentName + componentsInfo: FlickerComponentName ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { companion object { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt index f4dd7decb1b7..2357b0debb33 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/PipAppHelper.kt @@ -17,12 +17,16 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import android.graphics.Rect import android.media.session.MediaController import android.media.session.MediaSessionManager import android.os.SystemClock import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.FIND_TIMEOUT import com.android.server.wm.flicker.helpers.SYSTEMUI_PACKAGE +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.android.wm.shell.flicker.pip.tv.closeTvPipWindow import com.android.wm.shell.flicker.pip.tv.isFocusedOrHasFocusedChild @@ -31,7 +35,7 @@ import com.android.wm.shell.flicker.testapp.Components class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.PipActivity.LABEL, - Components.PipActivity.COMPONENT + Components.PipActivity.COMPONENT.toFlickerComponent() ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) @@ -62,7 +66,7 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( stringExtras: Map<String, String> ) { super.launchViaIntent(wmHelper, expectedWindowName, action, stringExtras) - wmHelper.waitFor { it.wmState.hasPipWindow() } + wmHelper.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } } private fun focusOnObject(selector: BySelector): Boolean { @@ -84,7 +88,11 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( clickObject(ENTER_PIP_BUTTON_ID) // Wait on WMHelper or simply wait for 3 seconds - wmHelper?.waitFor { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000) + wmHelper?.waitFor("hasPipWindow") { it.wmState.hasPipWindow() } ?: SystemClock.sleep(3_000) + // when entering pip, the dismiss button is visible at the start. to ensure the pip + // animation is complete, wait until the pip dismiss button is no longer visible. + // b/176822698: dismiss-only state will be removed in the future + uiDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "dismiss")), FIND_TIMEOUT) } fun clickStartMediaSessionButton() { @@ -113,61 +121,61 @@ class PipAppHelper(instrumentation: Instrumentation) : BaseAppHelper( } } + private fun getWindowRect(wmHelper: WindowManagerStateHelper): Rect { + val windowRegion = wmHelper.getWindowRegion(component) + require(!windowRegion.isEmpty) { + "Unable to find a PIP window in the current state" + } + return windowRegion.bounds + } + /** - * Expands the pip window and dismisses it by clicking on the X button. - * - * Note, currently the View coordinates reported by the accessibility are relative to - * the window, so the correct coordinates need to be calculated - * - * For example, in a PIP window located at Rect(508, 1444 - 1036, 1741), the - * dismiss button coordinates are shown as Rect(650, 0 - 782, 132), with center in - * Point(716, 66), instead of Point(970, 1403) - * - * See b/179337864 + * Taps the pip window and dismisses it by clicking on the X button. */ fun closePipWindow(wmHelper: WindowManagerStateHelper) { if (isTelevision) { uiDevice.closeTvPipWindow() } else { - expandPipWindow(wmHelper) - val exitPipObject = uiDevice.findObject(By.res(SYSTEMUI_PACKAGE, "dismiss")) - requireNotNull(exitPipObject) { "PIP window dismiss button not found" } - val dismissButtonBounds = exitPipObject.visibleBounds + val windowRect = getWindowRect(wmHelper) + uiDevice.click(windowRect.centerX(), windowRect.centerY()) + // search and interact with the dismiss button + val dismissSelector = By.res(SYSTEMUI_PACKAGE, "dismiss") + uiDevice.wait(Until.hasObject(dismissSelector), FIND_TIMEOUT) + val dismissPipObject = uiDevice.findObject(dismissSelector) + ?: error("PIP window dismiss button not found") + val dismissButtonBounds = dismissPipObject.visibleBounds uiDevice.click(dismissButtonBounds.centerX(), dismissButtonBounds.centerY()) } // Wait for animation to complete. - wmHelper.waitFor { !it.wmState.hasPipWindow() } + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } wmHelper.waitForHomeActivityVisible() } /** - * Click once on the PIP window to expand it + * Close the pip window by pressing the expand button */ - fun expandPipWindow(wmHelper: WindowManagerStateHelper) { - val windowRegion = wmHelper.getWindowRegion(component) - require(!windowRegion.isEmpty) { - "Unable to find a PIP window in the current state" - } - val windowRect = windowRegion.bounds + fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { + val windowRect = getWindowRect(wmHelper) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - // Ensure WindowManagerService wait until all animations have completed + // search and interact with the expand button + val expandSelector = By.res(SYSTEMUI_PACKAGE, "expand_button") + uiDevice.wait(Until.hasObject(expandSelector), FIND_TIMEOUT) + val expandPipObject = uiDevice.findObject(expandSelector) + ?: error("PIP window expand button not found") + val expandButtonBounds = expandPipObject.visibleBounds + uiDevice.click(expandButtonBounds.centerX(), expandButtonBounds.centerY()) + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() - mInstrumentation.uiAutomation.syncInputTransactions() } /** - * Double click on the PIP window to reopen to app + * Double click on the PIP window to expand it */ - fun expandPipWindowToApp(wmHelper: WindowManagerStateHelper) { - val windowRegion = wmHelper.getWindowRegion(component) - require(!windowRegion.isEmpty) { - "Unable to find a PIP window in the current state" - } - val windowRect = windowRegion.bounds + fun doubleClickPipWindow(wmHelper: WindowManagerStateHelper) { + val windowRect = getWindowRect(wmHelper) uiDevice.click(windowRect.centerX(), windowRect.centerY()) uiDevice.click(windowRect.centerX(), windowRect.centerY()) - wmHelper.waitFor { !it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt index ba13e38ae9e3..4d0fbc4a0e38 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SimpleAppHelper.kt @@ -17,10 +17,11 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.testapp.Components class SimpleAppHelper(instrumentation: Instrumentation) : BaseAppHelper( instrumentation, Components.SimpleActivity.LABEL, - Components.SimpleActivity.COMPONENT + Components.SimpleActivity.COMPONENT.toFlickerComponent() )
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt index 901b7a393291..0ec9b2d869a8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/helpers/SplitScreenHelper.kt @@ -17,32 +17,39 @@ package com.android.wm.shell.flicker.helpers import android.app.Instrumentation -import android.content.ComponentName +import android.content.res.Resources +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.parser.toFlickerComponent import com.android.wm.shell.flicker.testapp.Components class SplitScreenHelper( instrumentation: Instrumentation, activityLabel: String, - componentsInfo: ComponentName + componentsInfo: FlickerComponentName ) : BaseAppHelper(instrumentation, activityLabel, componentsInfo) { companion object { const val TEST_REPETITIONS = 1 const val TIMEOUT_MS = 3_000L + // TODO: remove all legacy split screen flicker tests when legacy split screen is fully + // deprecated. + fun isUsingLegacySplit(): Boolean = + Resources.getSystem().getBoolean(com.android.internal.R.bool.config_useLegacySplit) + fun getPrimary(instrumentation: Instrumentation): SplitScreenHelper = SplitScreenHelper(instrumentation, Components.SplitScreenActivity.LABEL, - Components.SplitScreenActivity.COMPONENT) + Components.SplitScreenActivity.COMPONENT.toFlickerComponent()) fun getSecondary(instrumentation: Instrumentation): SplitScreenHelper = SplitScreenHelper(instrumentation, Components.SplitScreenSecondaryActivity.LABEL, - Components.SplitScreenSecondaryActivity.COMPONENT) + Components.SplitScreenSecondaryActivity.COMPONENT.toFlickerComponent()) fun getNonResizeable(instrumentation: Instrumentation): SplitScreenHelper = SplitScreenHelper(instrumentation, Components.NonResizeableActivity.LABEL, - Components.NonResizeableActivity.COMPONENT) + Components.NonResizeableActivity.COMPONENT.toFlickerComponent()) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt index 4f12f2bb9f5f..bd44d082a1aa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenDockActivity.kt @@ -18,20 +18,21 @@ package com.android.wm.shell.flicker.legacysplitscreen import android.platform.test.annotations.Presubmit import android.view.Surface +import android.view.WindowManagerPolicyConstants +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.HOME_WINDOW_TITLE -import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -48,7 +49,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 +@Group4 class EnterSplitScreenDockActivity( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -60,16 +61,16 @@ class EnterSplitScreenDockActivity( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, LIVE_WALLPAPER_PACKAGE_NAME, - splitScreenApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, *HOME_WINDOW_TITLE) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, LIVE_WALLPAPER_COMPONENT, + splitScreenApp.component, FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, LAUNCHER_COMPONENT) @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test @@ -77,27 +78,39 @@ class EnterSplitScreenDockActivity( @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) } } + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0) // bugId = 179116910 + supportedRotations = listOf(Surface.ROTATION_0), // bugId = 179116910 + supportedNavigationModes = listOf( + WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY) ) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt index f91f634a00e5..625d48b8ab5a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenFromDetachedRecentTask.kt @@ -22,10 +22,11 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -42,6 +43,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@Group4 class EnterSplitScreenFromDetachedRecentTask( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -61,24 +63,34 @@ class EnterSplitScreenFromDetachedRecentTask( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, - splitScreenApp.defaultWindowName) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, + splitScreenApp.component) @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt index 85ded8a45233..2ed2806af528 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenLaunchToSide.kt @@ -22,18 +22,17 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 -import com.android.server.wm.flicker.appWindowBecomesVisible +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -49,7 +48,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group1 +@Group4 class EnterSplitScreenLaunchToSide( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -62,22 +61,22 @@ class EnterSplitScreenLaunchToSide( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - secondaryApp.defaultWindowName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, + secondaryApp.component, FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test - fun dockedStackSecondaryBoundsIsVisible() = - testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation, - secondaryApp.defaultWindowName) + fun dockedStackSecondaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + secondaryApp.component) @Presubmit @Test @@ -85,15 +84,35 @@ class EnterSplitScreenLaunchToSide( @Presubmit @Test - fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + // when the app is launched, first the activity becomes visible, then the + // SnapshotStartingWindow appears and then the app window becomes visible. + // Because we log WM once per frame, sometimes the activity and the window + // become visible in the same entry, sometimes not, thus it is not possible to + // assert the visibility of the activity here + this.isAppWindowInvisible(secondaryApp.component) + .then() + // during re-parenting, the window may disappear and reappear from the + // trace, this occurs because we log only 1x per frame + .notContains(secondaryApp.component, isOptional = true) + .then() + .isAppWindowVisible(secondaryApp.component) + } + } + + @Presubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt index e958bf39930e..ee6cf341c9ff 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenNotSupportNonResizable.kt @@ -22,11 +22,11 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group1 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.canSplitScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -50,7 +50,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@Group1 +@Group4 class EnterSplitScreenNotSupportNonResizable( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { @@ -70,12 +70,12 @@ class EnterSplitScreenNotSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, - nonResizeableApp.defaultWindowName, - splitScreenApp.defaultWindowName) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, + nonResizeableApp.component, + splitScreenApp.component) @Before override fun setup() { @@ -91,7 +91,12 @@ class EnterSplitScreenNotSupportNonResizable( @Presubmit @Test - fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt index d3acc82121b0..163b6ffda6e2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/EnterSplitScreenSupportNonResizable.kt @@ -25,8 +25,8 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -67,12 +67,12 @@ class EnterSplitScreenSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME, - nonResizeableApp.defaultWindowName, - splitScreenApp.defaultWindowName) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT, + nonResizeableApp.component, + splitScreenApp.component) @Before override fun setup() { @@ -88,16 +88,21 @@ class EnterSplitScreenSupportNonResizable( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun appWindowIsVisible() { testSpec.assertWmEnd { - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt index bad46836dcb7..2b629b0a7eb5 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitLegacySplitScreenFromBottom.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen -import android.platform.test.annotations.Presubmit +import android.platform.test.annotations.Postsubmit import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice @@ -24,15 +24,13 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.exitSplitScreenFromBottom import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -67,31 +65,52 @@ class ExitLegacySplitScreenFromBottom( } } transitions { - device.exitSplitScreenFromBottom() + device.exitSplitScreenFromBottom(wmHelper) } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, + splitScreenApp.component, secondaryApp.component, + FlickerComponentName.SNAPSHOT) - @Presubmit + @Postsubmit @Test - fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(DOCKED_STACK_DIVIDER) + fun layerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) + .then() + .isInvisible(DOCKED_STACK_DIVIDER_COMPONENT) + } + } @FlakyTest @Test - fun appWindowBecomesInVisible() = - testSpec.appWindowBecomesInVisible(secondaryApp.defaultWindowName) + fun appWindowBecomesInVisible() { + testSpec.assertWm { + this.isAppWindowVisible(secondaryApp.component) + .then() + .isAppWindowInvisible(secondaryApp.component) + } + } + + @Postsubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() - @Presubmit + @Postsubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - @Presubmit + @FlakyTest + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @FlakyTest @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt index 76dcd8b89242..95fe3bef4852 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ExitPrimarySplitScreenShowSecondaryFullscreen.kt @@ -24,15 +24,13 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -71,31 +69,52 @@ class ExitPrimarySplitScreenShowSecondaryFullscreen( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - splitScreenApp.defaultWindowName, secondaryApp.defaultWindowName, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, + splitScreenApp.component, secondaryApp.component, + FlickerComponentName.SNAPSHOT) - @FlakyTest(bugId = 175687842) + @Presubmit @Test - fun dockedStackDividerIsInvisible() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @FlakyTest @Test - fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName) + fun layerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(splitScreenApp.component) + .then() + .isInvisible(splitScreenApp.component) + } + } @FlakyTest @Test - fun appWindowBecomesInVisible() = - testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + fun appWindowBecomesInVisible() { + testSpec.assertWm { + this.isAppWindowVisible(splitScreenApp.component) + .then() + .isAppWindowInvisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() + + @Presubmit + @Test + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt index d0a64b3774c7..f7d628d48769 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentNotSupportNonResizable.kt @@ -23,15 +23,11 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -72,11 +68,11 @@ class LegacySplitScreenFromIntentNotSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, - nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + nonResizeableApp.component, splitScreenApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -92,44 +88,109 @@ class LegacySplitScreenFromIntentNotSupportNonResizable( @Presubmit @Test - fun resizableAppLayerBecomesInvisible() = - testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName) + fun resizableAppLayerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(splitScreenApp.component) + .then() + .isInvisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.notContains(nonResizeableApp.component) + .then() + .isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } + /** + * Assets that [splitScreenApp] exists at the start of the trace and, once it becomes + * invisible, it remains invisible until the end of the trace. + */ @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun resizableAppWindowBecomesInvisible() { + testSpec.assertWm { + // when the activity gets PAUSED the window may still be marked as visible + // it will be updated in the next log entry. This occurs because we record 1x + // per frame, thus ignore activity check here + this.isAppWindowVisible(splitScreenApp.component) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowInvisible(splitScreenApp.component) + } + } + /** + * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then + * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes + * visible, it remains visible until the end of the trace. + */ @Presubmit @Test - fun resizableAppWindowBecomesInvisible() = - testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + this.notContains(nonResizeableApp.component) + .then() + // we log once per frame, upon logging, window may be visible or not depending + // on what was processed until that moment. Both behaviors are correct + .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowVisible(nonResizeableApp.component) + } + } + /** + * Asserts that both the app window and the activity are visible at the end of the trace + */ @Presubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisibleAtEnd() { + testSpec.assertWmEnd { + isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @Presubmit @Test fun onlyNonResizableAppWindowIsVisibleAtEnd() { testSpec.assertWmEnd { - isInvisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowInvisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( - repetitions = SplitScreenHelper.TEST_REPETITIONS, - supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 + repetitions = SplitScreenHelper.TEST_REPETITIONS, + supportedRotations = listOf(Surface.ROTATION_0)) // b/178685668 } } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt index c26c05fa8db6..a5c6571f68de 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromIntentSupportNonResizable.kt @@ -23,13 +23,11 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -70,11 +68,11 @@ class LegacySplitScreenFromIntentSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, - nonResizeableApp.defaultWindowName, splitScreenApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + nonResizeableApp.component, splitScreenApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -90,27 +88,59 @@ class LegacySplitScreenFromIntentSupportNonResizable( @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } + /** + * Assets that [nonResizeableApp] doesn't exist at the start of the trace, then + * [nonResizeableApp] is created (visible or not) and, once [nonResizeableApp] becomes + * visible, it remains visible until the end of the trace. + */ @Presubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + this.notContains(nonResizeableApp.component) + .then() + // we log once per frame, upon logging, window may be visible or not depending + // on what was processed until that moment. Both behaviors are correct + .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppsWindowsAreVisibleAtEnd() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt index fb1758975442..6f486b0ddfea 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentNotSupportNonResizable.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -23,16 +24,12 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesInVisible -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsInvisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerNotExistsAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -73,11 +70,11 @@ class LegacySplitScreenFromRecentNotSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME, - splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -93,37 +90,73 @@ class LegacySplitScreenFromRecentNotSupportNonResizable( @Presubmit @Test - fun resizableAppLayerBecomesInvisible() = - testSpec.layerBecomesInvisible(splitScreenApp.defaultWindowName) + fun resizableAppLayerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(splitScreenApp.component) + .then() + .isInvisible(splitScreenApp.component) + } + } @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun resizableAppWindowBecomesInvisible() = - testSpec.appWindowBecomesInVisible(splitScreenApp.defaultWindowName) + fun resizableAppWindowBecomesInvisible() { + testSpec.assertWm { + // when the activity gets PAUSED the window may still be marked as visible + // it will be updated in the next log entry. This occurs because we record 1x + // per frame, thus ignore activity check here + this.isAppWindowVisible(splitScreenApp.component) + .then() + // immediately after the window (after onResume and before perform relayout) + // the activity is invisible. This may or not be logged, since we record 1x + // per frame, thus ignore activity check here + .isAppWindowInvisible(splitScreenApp.component) + } + } - @Presubmit + @Postsubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(nonResizeableApp.component) + .then() + .isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsInvisibleAtEnd() = testSpec.dockedStackDividerIsInvisible() + fun dockedStackDividerNotExistsAtEnd() = testSpec.dockedStackDividerNotExistsAtEnd() @Presubmit @Test fun onlyNonResizableAppWindowIsVisibleAtEnd() { testSpec.assertWmEnd { - isInvisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowInvisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt index a9c28efcdf44..f03c927b8d58 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenFromRecentSupportNonResizable.kt @@ -23,14 +23,12 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER -import com.android.wm.shell.flicker.dockedStackDividerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.resetMultiWindowConfig import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setSupportsNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper @@ -71,11 +69,11 @@ class LegacySplitScreenFromRecentSupportNonResizable( } } - override val ignoredWindows: List<String> - get() = listOf(DOCKED_STACK_DIVIDER, LAUNCHER_PACKAGE_NAME, LETTERBOX_NAME, TOAST_NAME, - splitScreenApp.defaultWindowName, nonResizeableApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(DOCKED_STACK_DIVIDER_COMPONENT, LAUNCHER_COMPONENT, LETTERBOX_COMPONENT, + TOAST_COMPONENT, splitScreenApp.component, nonResizeableApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Before override fun setup() { @@ -91,27 +89,60 @@ class LegacySplitScreenFromRecentSupportNonResizable( @Presubmit @Test - fun nonResizableAppLayerBecomesVisible() = - testSpec.layerBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppLayerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(nonResizeableApp.component) + .then() + .isVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun nonResizableAppWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(nonResizeableApp.defaultWindowName) + fun nonResizableAppWindowBecomesVisible() { + testSpec.assertWm { + // when the app is launched, first the activity becomes visible, then the + // SnapshotStartingWindow appears and then the app window becomes visible. + // Because we log WM once per frame, sometimes the activity and the window + // become visible in the same entry, sometimes not, thus it is not possible to + // assert the visibility of the activity here + this.isAppWindowInvisible(nonResizeableApp.component) + .then() + // during re-parenting, the window may disappear and reappear from the + // trace, this occurs because we log only 1x per frame + .notContains(nonResizeableApp.component, isOptional = true) + .then() + // if the window reappears after re-parenting it will most likely not + // be visible in the first log entry (because we log only 1x per frame) + .isAppWindowInvisible(nonResizeableApp.component, isOptional = true) + .then() + .isAppWindowVisible(nonResizeableApp.component) + } + } @Presubmit @Test - fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test fun bothAppsWindowsAreVisibleAtEnd() { testSpec.assertWmEnd { - isVisible(splitScreenApp.defaultWindowName) - isVisible(nonResizeableApp.defaultWindowName) + isAppWindowVisible(splitScreenApp.component) + isAppWindowVisible(nonResizeableApp.component) } } + @Presubmit + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt index a4d2ab51e358..2ccd03bf1d6a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenToLauncher.kt @@ -16,10 +16,9 @@ package com.android.wm.shell.flicker.legacysplitscreen +import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.support.test.launcherhelper.LauncherStrategyFactory import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter @@ -27,21 +26,19 @@ import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation -import com.android.server.wm.flicker.focusDoesNotChange +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.exitSplitScreen import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.openQuickStepAndClearRecentAppsFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.layerBecomesInvisible -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.dockedStackDividerBecomesInvisible import com.android.wm.shell.flicker.helpers.SimpleAppHelper import org.junit.FixMethodOrder @@ -62,8 +59,6 @@ import org.junit.runners.Parameterized class LegacySplitScreenToLauncher( testSpec: FlickerTestParameter ) : LegacySplitScreenTransition(testSpec) { - private val launcherPackageName = LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage private val testApp = SimpleAppHelper(instrumentation) override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit @@ -90,51 +85,69 @@ class LegacySplitScreenToLauncher( } } - override val ignoredWindows: List<String> - get() = listOf(launcherPackageName, WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible() + fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() @Presubmit @Test - fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation) + fun entireScreenCovered() = testSpec.entireScreenCovered() @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Presubmit @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() - @Presubmit + @Postsubmit @Test fun dockedStackDividerBecomesInvisible() = testSpec.dockedStackDividerBecomesInvisible() + @Postsubmit + @Test + fun layerBecomesInvisible() { + testSpec.assertLayers { + this.isVisible(testApp.component) + .then() + .isInvisible(testApp.component) + } + } + + @Postsubmit + @Test + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } + @Presubmit @Test - fun layerBecomesInvisible() = testSpec.layerBecomesInvisible(testApp.getPackage()) + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() - @FlakyTest(bugId = 151179149) + @Presubmit @Test - fun focusDoesNotChange() = testSpec.focusDoesNotChange() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt index e8d4d1e9ada2..661c8b69068e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/LegacySplitScreenTransition.kt @@ -31,11 +31,14 @@ import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.startRotation -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.getDevEnableNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.MultiWindowHelper.Companion.setDevEnableNonResizableMultiWindow import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.After +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test @@ -46,12 +49,17 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa protected val splitScreenApp = SplitScreenHelper.getPrimary(instrumentation) protected val secondaryApp = SplitScreenHelper.getSecondary(instrumentation) protected val nonResizeableApp = SplitScreenHelper.getNonResizeable(instrumentation) - protected val LAUNCHER_PACKAGE_NAME = LauncherStrategyFactory.getInstance(instrumentation) - .launcherStrategy.supportedLauncherPackage + protected val LAUNCHER_COMPONENT = FlickerComponentName("", + LauncherStrategyFactory.getInstance(instrumentation) + .launcherStrategy.supportedLauncherPackage) private var prevDevEnableNonResizableMultiWindow = 0 @Before open fun setup() { + // Only run legacy split tests when the system is using legacy split screen. + assumeTrue(SplitScreenHelper.isUsingLegacySplit()) + // Legacy split is having some issue with Shell transition, and will be deprecated soon. + assumeFalse(isShellTransitionsEnabled()) prevDevEnableNonResizableMultiWindow = getDevEnableNonResizableMultiWindow(context) if (prevDevEnableNonResizableMultiWindow != 0) { // Turn off the development option @@ -70,8 +78,9 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa * * b/182720234 */ - open val ignoredWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + open val ignoredWindows: List<FlickerComponentName> = listOf( + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) protected open val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { configuration -> @@ -138,9 +147,9 @@ abstract class LegacySplitScreenTransition(protected val testSpec: FlickerTestPa } companion object { - internal const val LIVE_WALLPAPER_PACKAGE_NAME = - "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2" - internal const val LETTERBOX_NAME = "Letterbox" - internal const val TOAST_NAME = "Toast" + internal val LIVE_WALLPAPER_COMPONENT = FlickerComponentName("", + "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2") + internal val LETTERBOX_COMPONENT = FlickerComponentName("", "Letterbox") + internal val TOAST_COMPONENT = FlickerComponentName("", "Toast") } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt index 05eb5f49a641..34eff80a04bc 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/OpenAppToLegacySplitScreen.kt @@ -24,15 +24,11 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.focusChanges +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.launchSplitScreen -import com.android.server.wm.flicker.layerBecomesVisible -import com.android.server.wm.flicker.noUncoveredRegions -import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.flicker.statusBarLayerIsVisible +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.appPairsDividerBecomesVisible import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder @@ -62,22 +58,28 @@ class OpenAppToLegacySplitScreen( } } - override val ignoredWindows: List<String> - get() = listOf(LAUNCHER_PACKAGE_NAME, splitScreenApp.defaultWindowName, - WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + override val ignoredWindows: List<FlickerComponentName> + get() = listOf(LAUNCHER_COMPONENT, splitScreenApp.component, + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) @FlakyTest @Test - fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(splitScreenApp.getPackage()) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(splitScreenApp.component) + .then() + .isAppWindowVisible(splitScreenApp.component) + } + } - @FlakyTest + @Presubmit @Test - fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation) + fun entireScreenCovered() = testSpec.entireScreenCovered() @Presubmit @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Presubmit @Test @@ -85,12 +87,27 @@ class OpenAppToLegacySplitScreen( @FlakyTest @Test - fun layerBecomesVisible() = testSpec.layerBecomesVisible(splitScreenApp.getPackage()) + fun layerBecomesVisible() { + testSpec.assertLayers { + this.isInvisible(splitScreenApp.component) + .then() + .isVisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + fun focusChanges() { + testSpec.assertEventLog { + this.focusChanges(splitScreenApp.`package`, + "recents_animation_input_consumer", "NexusLauncherActivity") + } + } - @FlakyTest(bugId = 151179149) + @Presubmit @Test - fun focusChanges() = testSpec.focusChanges(splitScreenApp.`package`, - "recents_animation_input_consumer", "NexusLauncherActivity") + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt index 3e83b6382939..58e1def6f37a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/ResizeLegacySplitScreen.kt @@ -27,24 +27,24 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.ImeAppHelper import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.resizeSplitScreen import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.traces.layers.getVisibleBounds -import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.server.wm.traces.parser.toFlickerComponent +import com.android.wm.shell.flicker.DOCKED_STACK_DIVIDER_COMPONENT import com.android.wm.shell.flicker.helpers.SimpleAppHelper +import com.android.wm.shell.flicker.testapp.Components import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -101,16 +101,16 @@ class ResizeLegacySplitScreen( } @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @FlakyTest(bugId = 156223549) @Test fun topAppWindowIsAlwaysVisible() { testSpec.assertWm { - this.showsAppWindow(sSimpleActivity) + this.isAppWindowVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent()) } } @@ -118,45 +118,43 @@ class ResizeLegacySplitScreen( @Test fun bottomAppWindowIsAlwaysVisible() { testSpec.assertWm { - this.showsAppWindow(sImeActivity) + this.isAppWindowVisible(Components.ImeActivity.COMPONENT.toFlickerComponent()) } } @Test - fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible() + fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() @Test - fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Test - fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.endRotation) + fun entireScreenCovered() = testSpec.entireScreenCovered() @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Test fun topAppLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(sSimpleActivity) + this.isVisible(Components.SimpleActivity.COMPONENT.toFlickerComponent()) } } @Test fun bottomAppLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(sImeActivity) + this.isVisible(Components.ImeActivity.COMPONENT.toFlickerComponent()) } } @Test fun dividerLayerIsAlwaysVisible() { testSpec.assertLayers { - this.isVisible(DOCKED_STACK_DIVIDER) + this.isVisible(DOCKED_STACK_DIVIDER_COMPONENT) } } @@ -166,7 +164,7 @@ class ResizeLegacySplitScreen( testSpec.assertLayersStart { val displayBounds = WindowUtils.displayBounds val dividerBounds = - entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds val topAppBounds = Region(0, 0, dividerBounds.right, dividerBounds.top + WindowUtils.dockedStackDividerInset) @@ -174,8 +172,10 @@ class ResizeLegacySplitScreen( dividerBounds.bottom - WindowUtils.dockedStackDividerInset, displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) - visibleRegion("SimpleActivity").coversExactly(topAppBounds) - visibleRegion("ImeActivity").coversExactly(bottomAppBounds) + visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) + .coversExactly(topAppBounds) + visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) + .coversExactly(bottomAppBounds) } } @@ -185,7 +185,7 @@ class ResizeLegacySplitScreen( testSpec.assertLayersStart { val displayBounds = WindowUtils.displayBounds val dividerBounds = - entry.getVisibleBounds(DOCKED_STACK_DIVIDER).bounds + layer(DOCKED_STACK_DIVIDER_COMPONENT).visibleRegion.region.bounds val topAppBounds = Region(0, 0, dividerBounds.right, dividerBounds.top + WindowUtils.dockedStackDividerInset) @@ -194,8 +194,10 @@ class ResizeLegacySplitScreen( displayBounds.right, displayBounds.bottom - WindowUtils.navigationBarHeight) - visibleRegion(sSimpleActivity).coversExactly(topAppBounds) - visibleRegion(sImeActivity).coversExactly(bottomAppBounds) + visibleRegion(Components.SimpleActivity.COMPONENT.toFlickerComponent()) + .coversExactly(topAppBounds) + visibleRegion(Components.ImeActivity.COMPONENT.toFlickerComponent()) + .coversExactly(bottomAppBounds) } } @@ -207,8 +209,6 @@ class ResizeLegacySplitScreen( } companion object { - private const val sSimpleActivity = "SimpleActivity" - private const val sImeActivity = "ImeActivity" private val startRatio = Rational(1, 3) private val stopRatio = Rational(2, 3) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt index 58482eaae3f5..8a50bc0b20cf 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppAndEnterSplitScreen.kt @@ -24,18 +24,16 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -66,38 +64,44 @@ class RotateOneLaunchedAppAndEnterSplitScreen( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @FlakyTest @Test - fun appWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(splitScreenApp.component) + .then() + .isAppWindowVisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt index 06828d6adb26..84676a9186be 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateOneLaunchedAppInSplitScreenMode.kt @@ -24,18 +24,16 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -66,35 +64,43 @@ class RotateOneLaunchedAppInSplitScreenMode( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = testSpec.dockedStackPrimaryBoundsIsVisible( - testSpec.config.startRotation, splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd( + testSpec.config.startRotation, splitScreenApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales( - testSpec.config.startRotation, testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales( - testSpec.config.startRotation, testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @FlakyTest @Test - fun appWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(splitScreenApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(splitScreenApp.component) + .then() + .isAppWindowVisible(splitScreenApp.component) + } + } + + @Presubmit + @Test + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt index f8e32bf171d8..2abdca9216f9 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppAndEnterSplitScreen.kt @@ -18,26 +18,23 @@ package com.android.wm.shell.flicker.legacysplitscreen import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -69,42 +66,63 @@ class RotateTwoLaunchedAppAndEnterSplitScreen( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test - fun dockedStackSecondaryBoundsIsVisible() = - testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation, - secondaryApp.defaultWindowName) + fun dockedStackSecondaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + secondaryApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales( - testSpec.config.startRotation, testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() + + @Presubmit + @Test + fun appWindowBecomesVisible() { + testSpec.assertWm { + // when the app is launched, first the activity becomes visible, then the + // SnapshotStartingWindow appears and then the app window becomes visible. + // Because we log WM once per frame, sometimes the activity and the window + // become visible in the same entry, sometimes not, thus it is not possible to + // assert the visibility of the activity here + this.isAppWindowInvisible(secondaryApp.component) + .then() + // during re-parenting, the window may disappear and reappear from the + // trace, this occurs because we log only 1x per frame + .notContains(secondaryApp.component, isOptional = true) + .then() + // if the window reappears after re-parenting it will most likely not + // be visible in the first log entry (because we log only 1x per frame) + .isAppWindowInvisible(secondaryApp.component, isOptional = true) + .then() + .isAppWindowVisible(secondaryApp.component) + } + } @Presubmit @Test - fun appWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName) + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt index cb246ca0b694..fe9b9f514015 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/legacysplitscreen/RotateTwoLaunchedAppInSplitScreenMode.kt @@ -24,20 +24,18 @@ import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group2 -import com.android.server.wm.flicker.appWindowBecomesVisible import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.endRotation import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.reopenAppFromOverview import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible -import com.android.wm.shell.flicker.dockedStackDividerIsVisible -import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisible -import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible +import com.android.wm.shell.flicker.dockedStackDividerIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackPrimaryBoundsIsVisibleAtEnd +import com.android.wm.shell.flicker.dockedStackSecondaryBoundsIsVisibleAtEnd import com.android.wm.shell.flicker.helpers.SplitScreenHelper import org.junit.FixMethodOrder import org.junit.Test @@ -74,44 +72,55 @@ class RotateTwoLaunchedAppInSplitScreenMode( @Presubmit @Test - fun dockedStackDividerIsVisible() = testSpec.dockedStackDividerIsVisible() + fun dockedStackDividerIsVisibleAtEnd() = testSpec.dockedStackDividerIsVisibleAtEnd() @Presubmit @Test - fun dockedStackPrimaryBoundsIsVisible() = - testSpec.dockedStackPrimaryBoundsIsVisible(testSpec.config.startRotation, - splitScreenApp.defaultWindowName) + fun dockedStackPrimaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackPrimaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + splitScreenApp.component) @Presubmit @Test - fun dockedStackSecondaryBoundsIsVisible() = - testSpec.dockedStackSecondaryBoundsIsVisible(testSpec.config.startRotation, - secondaryApp.defaultWindowName) + fun dockedStackSecondaryBoundsIsVisibleAtEnd() = + testSpec.dockedStackSecondaryBoundsIsVisibleAtEnd(testSpec.config.startRotation, + secondaryApp.component) - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest(bugId = 169271943) + @Presubmit @Test - fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, - testSpec.config.endRotation) + fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @FlakyTest @Test - fun appWindowBecomesVisible() = - testSpec.appWindowBecomesVisible(secondaryApp.defaultWindowName) + fun appWindowBecomesVisible() { + testSpec.assertWm { + this.isAppWindowInvisible(secondaryApp.component) + .then() + .isAppWindowVisible(secondaryApp.component) + } + } + + @Presubmit + @Test + fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() + + @Presubmit + @Test + fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() @Presubmit @Test - fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() @Presubmit @Test - fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = + super.visibleWindowsShownMoreThanOneConsecutiveEntry() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt index 2a660747bc1d..f9b08000290f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/CommonAssertions.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2021 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. @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:JvmName("CommonAssertions") package com.android.wm.shell.flicker.pip -internal const val PIP_WINDOW_TITLE = "PipMenuActivity" +internal const val PIP_WINDOW_COMPONENT = "PipMenuActivity" diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index b6af26060050..52a744f3897d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder import org.junit.FixMethodOrder @@ -32,8 +33,21 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch. + * Test entering pip from an app by interacting with the app UI + * * To run this test: `atest WMShellFlickerTests:EnterPipTest` + * + * Actions: + * Launch an app in full screen + * Press an "enter pip" button to put [pipApp] in pip mode + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @@ -41,49 +55,121 @@ import org.junit.runners.Parameterized @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true, stringExtras = emptyMap()) { transitions { - pipApp.clickEnterPipButton() - pipApp.expandPipWindow(wmHelper) + pipApp.clickEnterPipButton(wmHelper) } } - @FlakyTest + /** + * Checks [pipApp] window remains visible throughout the animation + */ + @Presubmit @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() + fun pipAppWindowAlwaysVisible() { + testSpec.assertWm { + this.isAppWindowVisible(pipApp.component) + } } + /** + * Checks [pipApp] layer remains visible throughout the animation + */ @Presubmit @Test - fun pipAppWindowAlwaysVisible() { + fun pipAppLayerAlwaysVisible() { + testSpec.assertLayers { + this.isVisible(pipApp.component) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipWindowRemainInsideVisibleBounds() { testSpec.assertWm { - this.showsAppWindow(pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } - @FlakyTest + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit @Test - fun pipLayerBecomesVisible() { + fun pipLayerRemainInsideVisibleBounds() { testSpec.assertLayers { - this.isVisible(pipApp.windowName) + coversAtMost(displayBounds, pipApp.component) } } - @FlakyTest + /** + * Checks that the visible region of [pipApp] always reduces during the animation + */ + @Presubmit @Test - fun pipWindowBecomesVisible() { - testSpec.assertWm { - invoke("pipWindowIsNotVisible") { - verify("Has no pip window").that(it.wmState.hasPipWindow()).isTrue() - }.then().invoke("pipWindowIsVisible") { - verify("Has pip window").that(it.wmState.hasPipWindow()).isTrue() + fun pipLayerReduces() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.coversAtMost(previous.visibleRegion.region) } } } + /** + * Checks that [pipApp] window becomes pinned + */ + @Presubmit + @Test + fun pipWindowBecomesPinned() { + testSpec.assertWm { + invoke("pipWindowIsNotPinned") { it.isNotPinned(pipApp.component) } + .then() + .invoke("pipWindowIsPinned") { it.isPinned(pipApp.component) } + } + } + + /** + * Checks [LAUNCHER_COMPONENT] layer remains visible throughout the animation + */ + @Presubmit + @Test + fun launcherLayerBecomesVisible() { + testSpec.assertLayers { + isInvisible(LAUNCHER_COMPONENT) + .then() + .isVisible(LAUNCHER_COMPONENT) + } + } + + /** + * Checks the focus doesn't change during the animation + */ + @FlakyTest + @Test + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index 3a1456e53f87..c8c3f4d64294 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -25,7 +25,11 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.navBarLayerRotatesAndScales +import com.android.server.wm.flicker.statusBarLayerRotatesScales +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.helpers.FixedAppHelper import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_PORTRAIT @@ -38,8 +42,22 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip with orientation changes. - * To run this test: `atest WMShellFlickerTests:PipOrientationTest` + * Test entering pip while changing orientation (from app in landscape to pip window in portrait) + * + * To run this test: `atest EnterPipToOtherOrientationTest:EnterPipToOtherOrientationTest` + * + * Actions: + * Launch [testApp] on a fixed portrait orientation + * Launch [pipApp] on a fixed landscape orientation + * Broadcast action [ACTION_ENTER_PIP] to enter pip mode + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @@ -53,6 +71,9 @@ class EnterPipToOtherOrientationTest( private val startingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_90) private val endingBounds = WindowUtils.getDisplayBounds(Surface.ROTATION_0) + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { configuration -> setupAndTeardown(this, configuration) @@ -79,65 +100,125 @@ class EnterPipToOtherOrientationTest( broadcastActionTrigger.doAction(ACTION_ENTER_PIP) wmHelper.waitFor { it.wmState.hasPipWindow() } wmHelper.waitForAppTransitionIdle() + // during rotation the status bar becomes invisible and reappears at the end + wmHelper.waitForNavBarStatusBarVisible() } } + /** + * Checks that the [FlickerComponentName.NAV_BAR] has the correct position at + * the start and end of the transition + */ @FlakyTest @Test - override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() - @FlakyTest + /** + * Checks that the [FlickerComponentName.STATUS_BAR] has the correct position at + * the start and end of the transition + */ + @Presubmit @Test - override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - @FlakyTest + /** + * Checks that all parts of the screen are covered at the start and end of the transition + * + * TODO b/197726599 Prevents all states from being checked + */ + @Presubmit @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() - } + override fun entireScreenCovered() = testSpec.entireScreenCovered(allStates = false) + /** + * Checks [pipApp] window remains visible and on top throughout the transition + */ @Presubmit @Test fun pipAppWindowIsAlwaysOnTop() { testSpec.assertWm { - showsAppWindowOnTop(pipApp.defaultWindowName) + isAppWindowOnTop(pipApp.component) } } + /** + * Checks that [testApp] window is not visible at the start + */ @Presubmit @Test - fun pipAppHidesTestApp() { + fun testAppWindowInvisibleOnStart() { testSpec.assertWmStart { - isInvisible(testApp.defaultWindowName) + isAppWindowInvisible(testApp.component) } } + /** + * Checks that [testApp] window is visible at the end + */ @Presubmit @Test - fun testAppWindowIsVisible() { + fun testAppWindowVisibleOnEnd() { testSpec.assertWmEnd { - isVisible(testApp.defaultWindowName) + isAppWindowVisible(testApp.component) + } + } + + /** + * Checks that [testApp] layer is not visible at the start + */ + @Presubmit + @Test + fun testAppLayerInvisibleOnStart() { + testSpec.assertLayersStart { + isInvisible(testApp.component) + } + } + + /** + * Checks that [testApp] layer is visible at the end + */ + @Presubmit + @Test + fun testAppLayerVisibleOnEnd() { + testSpec.assertLayersEnd { + isVisible(testApp.component) } } + /** + * Checks that the visible region of [pipApp] covers the full display area at the start of + * the transition + */ @Presubmit @Test - fun pipAppLayerHidesTestApp() { + fun pipAppLayerCoversFullScreenOnStart() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversExactly(startingBounds) - isInvisible(testApp.defaultWindowName) + visibleRegion(pipApp.component).coversExactly(startingBounds) } } + /** + * Checks that the visible region of [testApp] plus the visible region of [pipApp] + * cover the full display area at the end of the transition + */ @Presubmit @Test - fun testAppLayerCoversFullScreen() { + fun testAppPlusPipLayerCoversFullScreenOnEnd() { testSpec.assertLayersEnd { - visibleRegion(testApp.defaultWindowName).coversExactly(endingBounds) + val pipRegion = visibleRegion(pipApp.component).region + visibleRegion(testApp.component) + .plus(pipRegion) + .coversExactly(endingBounds) } } companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt new file mode 100644 index 000000000000..64b7eb53bd6f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipToAppTransition.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2020 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.pip + +import android.platform.test.annotations.Presubmit +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import org.junit.Test + +/** + * Base class for pip expand tests + */ +abstract class ExitPipToAppTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + protected val testApp = FixedAppHelper(instrumentation) + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipAppWindowRemainInsideVisibleBounds() { + testSpec.assertWm { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipAppLayerRemainInsideVisibleBounds() { + testSpec.assertLayers { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks both app windows are visible at the start of the transition (with [pipApp] on top). + * Then, during the transition, [testApp] becomes invisible and [pipApp] remains visible + */ + @Presubmit + @Test + open fun showBothAppWindowsThenHidePip() { + testSpec.assertWm { + // when the activity is STOPPING, sometimes it becomes invisible in an entry before + // the window, sometimes in the same entry. This occurs because we log 1x per frame + // thus we ignore activity here + isAppWindowVisible(testApp.component) + .isAppWindowOnTop(pipApp.component) + .then() + .isAppWindowInvisible(testApp.component) + .isAppWindowVisible(pipApp.component) + } + } + + /** + * Checks both app layers are visible at the start of the transition. Then, during the + * transition, [testApp] becomes invisible and [pipApp] remains visible + */ + @Presubmit + @Test + open fun showBothAppLayersThenHidePip() { + testSpec.assertLayers { + isVisible(testApp.component) + .isVisible(pipApp.component) + .then() + .isInvisible(testApp.component) + .isVisible(pipApp.component) + } + } + + /** + * Checks that the visible region of [testApp] plus the visible region of [pipApp] + * cover the full display area at the start of the transition + */ + @Presubmit + @Test + open fun testPlusPipAppsCoverFullScreenAtStart() { + testSpec.assertLayersStart { + val pipRegion = visibleRegion(pipApp.component).region + visibleRegion(testApp.component) + .plus(pipRegion) + .coversExactly(displayBounds) + } + } + + /** + * Checks that the visible region of [pipApp] covers the full display area at the end of + * the transition + */ + @Presubmit + @Test + open fun pipAppCoversFullScreenAtEnd() { + testSpec.assertLayersEnd { + visibleRegion(pipApp.component).coversExactly(displayBounds) + } + } + + /** + * Checks that the visible region of [pipApp] always expands during the animation + */ + @Presubmit + @Test + open fun pipLayerExpands() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.coversAtLeast(previous.visibleRegion.region) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt index eae7e973711c..5207fed59208 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipTransition.kt @@ -20,15 +20,16 @@ import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.LAUNCHER_COMPONENT import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.focusChanges import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.startRotation import org.junit.Test -import org.junit.runners.Parameterized -abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { +/** + * Base class for exiting pip (closing pip window) without returning to the app + */ +abstract class ExitPipTransition(testSpec: FlickerTestParameter) : PipTransition(testSpec) { override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true) { configuration -> setup { @@ -43,37 +44,49 @@ abstract class PipCloseTransition(testSpec: FlickerTestParameter) : PipTransitio } } + /** + * Checks that [pipApp] window is pinned and visible at the start and then becomes + * unpinned and invisible at the same moment, and remains unpinned and invisible + * until the end of the transition + */ @Presubmit @Test open fun pipWindowBecomesInvisible() { testSpec.assertWm { - this.showsAppWindow(PIP_WINDOW_TITLE) - .then() - .hidesAppWindow(PIP_WINDOW_TITLE) + this.invoke("hasPipWindow") { + it.isPinned(pipApp.component).isAppWindowVisible(pipApp.component) + }.then().invoke("!hasPipWindow") { + it.isNotPinned(pipApp.component).isAppWindowInvisible(pipApp.component) + } } } + /** + * Checks that [pipApp] and [LAUNCHER_COMPONENT] layers are visible at the start + * of the transition. Then [pipApp] layer becomes invisible, and remains invisible + * until the end of the transition + */ @Presubmit @Test open fun pipLayerBecomesInvisible() { testSpec.assertLayers { - this.isVisible(PIP_WINDOW_TITLE) + this.isVisible(pipApp.component) + .isVisible(LAUNCHER_COMPONENT) .then() - .isInvisible(PIP_WINDOW_TITLE) + .isInvisible(pipApp.component) + .isVisible(LAUNCHER_COMPONENT) } } + /** + * Checks that the focus changes between the [pipApp] window and the launcher when + * closing the pip window + */ @FlakyTest(bugId = 151179149) @Test - open fun focusChanges() = testSpec.focusChanges(pipApp.launcherName, "NexusLauncherActivity") - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) + open fun focusChanges() { + testSpec.assertEventLog { + this.focusChanges(pipApp.launcherName, "NexusLauncherActivity") } } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt new file mode 100644 index 000000000000..b53342d6f2f7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 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.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window back to full screen via the expand button + * + * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Launch another full screen mode [testApp] + * Expand [pipApp] app to full screen by clicking on the pip window and + * then on the expand button + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class ExitPipViaExpandButtonClickTest( + testSpec: FlickerTestParameter +) : ExitPipToAppTransition(testSpec) { + + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition(eachRun = true) { + setup { + eachRun { + // launch an app behind the pip one + testApp.launchViaIntent(wmHelper) + } + } + transitions { + // This will bring PipApp to fullscreen + pipApp.expandPipWindowToApp(wmHelper) + // Wait until the other app is no longer visible + wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName()) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 00e50e7fe3b5..1fec3cf85214 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterExitPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -24,88 +23,62 @@ import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.helpers.FixedAppHelper import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch and exit. - * To run this test: `atest WMShellFlickerTests:EnterExitPipTest` + * Test expanding a pip window back to full screen via an intent + * + * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Launch another full screen mode [testApp] + * Expand [pipApp] app to full screen via an intent + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class EnterExitPipTest( - testSpec: FlickerTestParameter -) : PipTransition(testSpec) { - private val testApp = FixedAppHelper(instrumentation) +class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransition(testSpec) { + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = true) { setup { eachRun { + // launch an app behind the pip one testApp.launchViaIntent(wmHelper) } } transitions { // This will bring PipApp to fullscreen pipApp.launchViaIntent(wmHelper) + // Wait until the other app is no longer visible + wmHelper.waitForSurfaceAppeared(testApp.component.toWindowName()) } } - @Presubmit - @Test - fun pipAppRemainInsideVisibleBounds() { - testSpec.assertWm { - coversAtMost(displayBounds, pipApp.defaultWindowName) - } - } - - @Presubmit - @Test - fun showBothAppWindowsThenHidePip() { - testSpec.assertWm { - showsAppWindow(testApp.defaultWindowName) - .showsAppWindowOnTop(pipApp.defaultWindowName) - .then() - .hidesAppWindow(testApp.defaultWindowName) - } - } - - @Presubmit - @Test - fun showBothAppLayersThenHidePip() { - testSpec.assertLayers { - isVisible(testApp.defaultWindowName) - .isVisible(pipApp.defaultWindowName) - .then() - .isInvisible(testApp.defaultWindowName) - } - } - - @Presubmit - @Test - fun testAppCoversFullScreenWithPipOnDisplay() { - testSpec.assertLayersStart { - visibleRegion(testApp.defaultWindowName).coversExactly(displayBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds) - } - } - - @Presubmit - @Test - fun pipAppCoversFullScreen() { - testSpec.assertLayersEnd { - visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds) - } - } - companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt new file mode 100644 index 000000000000..73626c23065a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithDismissButtonTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 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.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test closing a pip window via the dismiss button + * + * To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Click on the pip window + * Click on dismiss button and wait window disappear + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class ExitPipWithDismissButtonTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = { + super.transition(this, it) + transitions { + pipApp.closePipWindow(wmHelper) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt index 524a1b404591..9e43deef8d99 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipCloseWithSwipeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipWithSwipeDownTest.kt @@ -22,9 +22,9 @@ import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import org.junit.FixMethodOrder import org.junit.Test @@ -33,42 +33,58 @@ import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipCloseWithSwipe` + * Test closing a pip window by swiping it to the bottom-center of the screen + * + * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Swipe the pip window to the bottom-center of the screen and wait it disappear + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition(testSpec) { +class ExitPipWithSwipeDownTest(testSpec: FlickerTestParameter) : ExitPipTransition(testSpec) { override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = { - super.transition(this, it) + get() = { args -> + super.transition(this, args) transitions { val pipRegion = wmHelper.getWindowRegion(pipApp.component).bounds val pipCenterX = pipRegion.centerX() val pipCenterY = pipRegion.centerY() val displayCenterX = device.displayWidth / 2 - device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 5) + device.swipe(pipCenterX, pipCenterY, displayCenterX, device.displayHeight, 10) + wmHelper.waitFor("!hasPipWindow") { !it.wmState.hasPipWindow() } + wmHelper.waitForWindowSurfaceDisappeared(pipApp.component) + wmHelper.waitForAppTransitionIdle() } } @Presubmit @Test - override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible() + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() @Presubmit @Test - override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible() + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() @Presubmit @Test - override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() @Presubmit @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() @FlakyTest @Test @@ -80,14 +96,29 @@ class PipCloseWithSwipeTest(testSpec: FlickerTestParameter) : PipCloseTransition @Presubmit @Test - override fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0) + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - override fun noUncoveredRegions() = super.noUncoveredRegions() + override fun entireScreenCovered() = super.entireScreenCovered() @Presubmit @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 20) + } + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt new file mode 100644 index 000000000000..d0fee9a82093 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 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.pip + +import android.platform.test.annotations.Presubmit +import android.view.Surface +import androidx.test.filters.FlakyTest +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.LAUNCHER_COMPONENT +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window by double clicking it + * + * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest` + * + * Actions: + * Launch an app in pip mode [pipApp], + * Expand [pipApp] app to its maximum pip size by double clicking on it + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class ExpandPipOnDoubleClickTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition(eachRun = true) { + transitions { + pipApp.doubleClickPipWindow(wmHelper) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipWindowRemainInsideVisibleBounds() { + testSpec.assertWm { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + fun pipLayerRemainInsideVisibleBounds() { + testSpec.assertLayers { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks [pipApp] window remains visible throughout the animation + */ + @Presubmit + @Test + fun pipWindowIsAlwaysVisible() { + testSpec.assertWm { + isAppWindowVisible(pipApp.component) + } + } + + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit + @Test + fun pipLayerIsAlwaysVisible() { + testSpec.assertLayers { + isVisible(pipApp.component) + } + } + + /** + * Checks that the visible region of [pipApp] always expands during the animation + */ + @Presubmit + @Test + fun pipLayerExpands() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + current.visibleRegion.coversAtLeast(previous.visibleRegion.region) + } + } + } + + /** + * Checks [pipApp] window remains pinned throughout the animation + */ + @Presubmit + @Test + fun windowIsAlwaysPinned() { + testSpec.assertWm { + this.invoke("hasPipWindow") { it.isPinned(pipApp.component) } + } + } + + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit + @Test + fun launcherIsAlwaysVisible() { + testSpec.assertLayers { + isVisible(LAUNCHER_COMPONENT) + } + } + + /** + * Checks that the focus doesn't change between windows during the transition + */ + @FlakyTest + @Test + fun focusDoesNotChange() { + testSpec.assertEventLog { + this.focusDoesNotChange() + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), + repetitions = 5) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt new file mode 100644 index 000000000000..0ab857d755ee --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 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.pip + +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.traces.RegionSubject +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test Pip movement with Launcher shelf height change (decrease). + * + * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest` + * + * Actions: + * Launch [pipApp] in pip mode + * Launch [testApp] + * Press home + * Check if pip window moves down (visually) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Group3 +class MovePipDownShelfHeightChangeTest( + testSpec: FlickerTestParameter +) : MovePipShelfHeightTransition(testSpec) { + /** + * Defines the transition used to run the test + */ + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit + get() = buildTransition(eachRun = false) { + teardown { + eachRun { + testApp.launchViaIntent(wmHelper) + } + test { + testApp.exit(wmHelper) + } + } + transitions { + taplInstrumentation.pressHome() + } + } + + override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { + current.isHigherOrEqual(previous.region) + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance().getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0), repetitions = 5) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt new file mode 100644 index 000000000000..6e0324c17272 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021 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.pip + +import android.platform.test.annotations.Presubmit +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.traces.RegionSubject +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import org.junit.Test + +/** + * Base class for pip tests with Launcher shelf height change + */ +abstract class MovePipShelfHeightTransition( + testSpec: FlickerTestParameter +) : PipTransition(testSpec) { + protected val taplInstrumentation = LauncherInstrumentation() + protected val testApp = FixedAppHelper(instrumentation) + + /** + * Checks if the window movement direction is valid + */ + protected abstract fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) + + /** + * Checks [pipApp] window remains visible throughout the animation + */ + @Presubmit + @Test + open fun pipWindowIsAlwaysVisible() { + testSpec.assertWm { + isAppWindowVisible(pipApp.component) + } + } + + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit + @Test + open fun pipLayerIsAlwaysVisible() { + testSpec.assertLayers { + isVisible(pipApp.component) + } + } + + /** + * Checks that the pip app window remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipWindowRemainInsideVisibleBounds() { + testSpec.assertWm { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the pip app layer remains inside the display bounds throughout the whole + * animation + */ + @Presubmit + @Test + open fun pipLayerRemainInsideVisibleBounds() { + testSpec.assertLayers { + coversAtMost(displayBounds, pipApp.component) + } + } + + /** + * Checks that the visible region of [pipApp] always moves in the correct direction + * during the animation. + */ + @Presubmit + @Test + open fun pipWindowMoves() { + val windowName = pipApp.component.toWindowName() + testSpec.assertWm { + val pipWindowList = this.windowStates { it.name.contains(windowName) && it.isVisible } + pipWindowList.zipWithNext { previous, current -> + assertRegionMovement(previous.frame, current.frame) + } + } + } + + /** + * Checks that the visible region of [pipApp] always moves up during the animation + */ + @Presubmit + @Test + open fun pipLayerMoves() { + val layerName = pipApp.component.toLayerName() + testSpec.assertLayers { + val pipLayerList = this.layers { it.name.contains(layerName) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + assertRegionMovement(previous.visibleRegion, current.visibleRegion) + } + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt index 1294ac93f647..e507edfda48c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipShelfHeightTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpShelfHeightChangeTest.kt @@ -16,36 +16,49 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice -import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory import com.android.server.wm.flicker.annotation.Group3 import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.wm.shell.flicker.helpers.FixedAppHelper -import com.google.common.truth.Truth +import com.android.server.wm.flicker.traces.RegionSubject import org.junit.FixMethodOrder -import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters import org.junit.runners.Parameterized /** - * Test Pip movement with Launcher shelf height change. - * To run this test: `atest WMShellFlickerTests:PipShelfHeightTest` + * Test Pip movement with Launcher shelf height change (increase). + * + * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest` + * + * Actions: + * Launch [pipApp] in pip mode + * Press home + * Launch [testApp] + * Check if pip window moves up (visually) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @Group3 -class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - private val taplInstrumentation = LauncherInstrumentation() - private val testApp = FixedAppHelper(instrumentation) - +class MovePipUpShelfHeightChangeTest( + testSpec: FlickerTestParameter +) : MovePipShelfHeightTransition(testSpec) { + /** + * Defines the transition used to run the test + */ override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = false) { teardown { @@ -61,33 +74,17 @@ class PipShelfHeightTest(testSpec: FlickerTestParameter) : PipTransition(testSpe } } - @Presubmit - @Test - fun pipAlwaysVisible() = testSpec.assertWm { this.showsAppWindow(pipApp.windowName) } - - @Presubmit - @Test - fun pipLayerInsideDisplay() { - testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversAtMost(displayBounds) - } - } - - @Presubmit - @Test - fun pipWindowMovesUp() = testSpec.assertWmEnd { - val initialState = this.trace?.first()?.wmState - ?: error("Trace should not be empty") - val startPos = initialState.pinnedWindows.first().frame - val currPos = this.wmState.pinnedWindows.first().frame - val subject = Truth.assertWithMessage("Pip should have moved up") - subject.that(currPos.top).isGreaterThan(startPos.top) - subject.that(currPos.bottom).isGreaterThan(startPos.bottom) - subject.that(currPos.left).isEqualTo(startPos.left) - subject.that(currPos.right).isEqualTo(startPos.right) + override fun assertRegionMovement(previous: RegionSubject, current: RegionSubject) { + current.isLowerOrEqual(previous.region) } companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): List<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt index d88f94d5954a..aba8aced298f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipKeyboardTest.kt @@ -22,12 +22,12 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.startRotation -import com.android.wm.shell.flicker.IME_WINDOW_NAME +import com.android.server.wm.traces.common.FlickerComponentName import com.android.wm.shell.flicker.helpers.ImeAppHelper import org.junit.FixMethodOrder import org.junit.Test @@ -43,7 +43,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 +@Group4 class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val imeApp = ImeAppHelper(instrumentation) @@ -79,7 +79,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) fun pipInVisibleBounds() { testSpec.assertWm { val displayBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation) - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } @@ -90,7 +90,7 @@ class PipKeyboardTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) @Test fun pipIsAboveAppWindow() { testSpec.assertWmTag(TAG_IME_VISIBLE) { - isAboveWindow(IME_WINDOW_NAME, pipApp.defaultWindowName) + isAboveWindow(FlickerComponentName.IME, pipApp.component) } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt index 6833b96a802b..9bea5c03dadb 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipLegacySplitScreenTest.kt @@ -23,15 +23,20 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.launchSplitScreen import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.wm.shell.flicker.helpers.ImeAppHelper -import com.android.wm.shell.flicker.helpers.FixedAppHelper import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome +import com.android.wm.shell.flicker.helpers.BaseAppHelper.Companion.isShellTransitionsEnabled +import com.android.wm.shell.flicker.helpers.FixedAppHelper +import com.android.wm.shell.flicker.helpers.ImeAppHelper +import com.android.wm.shell.flicker.helpers.SplitScreenHelper import com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP +import org.junit.Assume.assumeFalse +import org.junit.Assume.assumeTrue +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -46,12 +51,19 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@FlakyTest(bugId = 161435597) -@Group3 +@Group4 class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val imeApp = ImeAppHelper(instrumentation) private val testApp = FixedAppHelper(instrumentation) + @Before + open fun setup() { + // Only run legacy split tests when the system is using legacy split screen. + assumeTrue(SplitScreenHelper.isUsingLegacySplit()) + // Legacy split is having some issue with Shell transition, and will be deprecated soon. + assumeFalse(isShellTransitionsEnabled()) + } + override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = { withTestName { testSpec.name } @@ -80,11 +92,11 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t } } - @Presubmit + @FlakyTest(bugId = 161435597) @Test fun pipWindowInsideDisplayBounds() { testSpec.assertWm { - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } @@ -92,25 +104,17 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t @Test fun bothAppWindowsVisible() { testSpec.assertWmEnd { - isVisible(testApp.defaultWindowName) - isVisible(imeApp.defaultWindowName) - noWindowsOverlap(testApp.defaultWindowName, imeApp.defaultWindowName) + isAppWindowVisible(testApp.component) + isAppWindowVisible(imeApp.component) + doNotOverlap(testApp.component, imeApp.component) } } - @Presubmit - @Test - override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() - - @Presubmit - @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() - - @Presubmit + @FlakyTest(bugId = 161435597) @Test fun pipLayerInsideDisplayBounds() { testSpec.assertLayers { - coversAtMost(displayBounds, pipApp.defaultWindowName) + coversAtMost(displayBounds, pipApp.component) } } @@ -118,18 +122,14 @@ class PipLegacySplitScreenTest(testSpec: FlickerTestParameter) : PipTransition(t @Test fun bothAppLayersVisible() { testSpec.assertLayersEnd { - visibleRegion(testApp.defaultWindowName).coversAtMost(displayBounds) - visibleRegion(imeApp.defaultWindowName).coversAtMost(displayBounds) + visibleRegion(testApp.component).coversAtMost(displayBounds) + visibleRegion(imeApp.component).coversAtMost(displayBounds) } } - @Presubmit - @Test - override fun navBarLayerIsAlwaysVisible() = super.navBarLayerIsAlwaysVisible() - - @Presubmit + @FlakyTest(bugId = 161435597) @Test - override fun statusBarLayerIsAlwaysVisible() = super.statusBarLayerIsAlwaysVisible() + override fun entireScreenCovered() = super.entireScreenCovered() companion object { const val TEST_REPETITIONS = 2 diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index d531af28e2ad..669f37ad1e72 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -23,13 +23,13 @@ import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.endRotation +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.noUncoveredRegions import com.android.server.wm.flicker.startRotation import com.android.server.wm.flicker.statusBarLayerRotatesScales import com.android.wm.shell.flicker.helpers.FixedAppHelper @@ -41,17 +41,32 @@ import org.junit.runners.Parameterized /** * Test Pip Stack in bounds after rotations. + * * To run this test: `atest WMShellFlickerTests:PipRotationTest` + * + * Actions: + * Launch a [pipApp] in pip mode + * Launch another app [fixedApp] (appears below pip) + * Rotate the screen from [testSpec.config.startRotation] to [testSpec.config.endRotation] + * (usually, 0->90 and 90->0) + * + * Notes: + * 1. Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited from [PipTransition] + * 2. Part of the test setup occurs automatically via + * [com.android.server.wm.flicker.TransitionRunnerWithRules], + * including configuring navigation mode, initial orientation and ensuring no + * apps are running before setup */ @RequiresDevice @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 +@Group4 class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { private val fixedApp = FixedAppHelper(instrumentation) - private val startingBounds = WindowUtils.getDisplayBounds(testSpec.config.startRotation) - private val endingBounds = WindowUtils.getDisplayBounds(testSpec.config.endRotation) + private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.config.startRotation) + private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.config.endRotation) override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit get() = buildTransition(eachRun = false) { configuration -> @@ -66,49 +81,104 @@ class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) transitions { setRotation(configuration.endRotation) } - teardown { - eachRun { - setRotation(Surface.ROTATION_0) - } - } } - @FlakyTest(bugId = 185400889) + /** + * Checks that all parts of the screen are covered at the start and end of the transition + */ + @Presubmit @Test - override fun noUncoveredRegions() = testSpec.noUncoveredRegions(testSpec.config.startRotation, - testSpec.config.endRotation, allStates = false) + override fun entireScreenCovered() = testSpec.entireScreenCovered() + /** + * Checks the position of the navigation bar at the start and end of the transition + */ @FlakyTest @Test - override fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, - testSpec.config.endRotation) + override fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() + /** + * Checks the position of the status bar at the start and end of the transition + */ @Presubmit @Test - override fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, - testSpec.config.endRotation) + override fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() - @FlakyTest(bugId = 185400889) + /** + * Checks that [fixedApp] layer is within [screenBoundsStart] at the start of the transition + */ + @Presubmit @Test fun appLayerRotates_StartingBounds() { testSpec.assertLayersStart { - visibleRegion(fixedApp.defaultWindowName).coversExactly(startingBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + visibleRegion(fixedApp.component).coversExactly(screenBoundsStart) } } - @FlakyTest(bugId = 185400889) + /** + * Checks that [fixedApp] layer is within [screenBoundsEnd] at the end of the transition + */ + @Presubmit @Test fun appLayerRotates_EndingBounds() { testSpec.assertLayersEnd { - visibleRegion(fixedApp.defaultWindowName).coversExactly(endingBounds) - visibleRegion(pipApp.defaultWindowName).coversAtMost(endingBounds) + visibleRegion(fixedApp.component).coversExactly(screenBoundsEnd) + } + } + + /** + * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition + */ + @Presubmit + @Test + fun pipLayerRotates_StartingBounds() { + testSpec.assertLayersStart { + visibleRegion(pipApp.component).coversAtMost(screenBoundsStart) + } + } + + /** + * Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition + */ + @Presubmit + @Test + fun pipLayerRotates_EndingBounds() { + testSpec.assertLayersEnd { + visibleRegion(pipApp.component).coversAtMost(screenBoundsEnd) + } + } + + /** + * Ensure that the [pipApp] window does not obscure the [fixedApp] at the start of the + * transition + */ + @Presubmit + @Test + fun pipIsAboveFixedAppWindow_Start() { + testSpec.assertWmStart { + isAboveWindow(pipApp.component, fixedApp.component) + } + } + + /** + * Ensure that the [pipApp] window does not obscure the [fixedApp] at the end of the + * transition + */ + @Presubmit + @Test + fun pipIsAboveFixedAppWindow_End() { + testSpec.assertWmEnd { + isAboveWindow(pipApp.component, fixedApp.component) } } companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTestParameter> { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt deleted file mode 100644 index 55e5c4128967..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipToAppTest.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2020 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.pip - -import android.view.Surface -import androidx.test.filters.FlakyTest -import androidx.test.filters.RequiresDevice -import com.android.server.wm.flicker.FlickerParametersRunnerFactory -import com.android.server.wm.flicker.FlickerTestParameter -import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 -import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.focusChanges -import com.android.server.wm.flicker.helpers.setRotation -import com.android.server.wm.flicker.startRotation -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test Pip launch. - * To run this test: `atest WMShellFlickerTests:PipToAppTest` - */ -@RequiresDevice -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 -class PipToAppTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { - override val transition: FlickerBuilder.(Map<String, Any?>) -> Unit - get() = buildTransition(eachRun = true) { configuration -> - setup { - eachRun { - this.setRotation(configuration.startRotation) - } - } - teardown { - eachRun { - this.setRotation(Surface.ROTATION_0) - } - } - transitions { - pipApp.expandPipWindowToApp(wmHelper) - } - } - - @FlakyTest - @Test - fun appReplacesPipWindow() { - testSpec.assertWm { - this.showsAppWindow(PIP_WINDOW_TITLE) - .then() - .showsAppWindowOnTop(pipApp.launcherName) - } - } - - @FlakyTest - @Test - fun appReplacesPipLayer() { - testSpec.assertLayers { - this.isVisible(PIP_WINDOW_TITLE) - .then() - .isVisible(pipApp.launcherName) - } - } - - @FlakyTest - @Test - fun testAppCoversFullScreen() { - testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversExactly(displayBounds) - } - } - - @FlakyTest(bugId = 151179149) - @Test - fun focusChanges() = testSpec.focusChanges("NexusLauncherActivity", - pipApp.launcherName, "NexusLauncherActivity") - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun getParams(): List<FlickerTestParameter> { - return FlickerTestParameterFactory.getInstance() - .getConfigNonRotationTests(supportedRotations = listOf(Surface.ROTATION_0), - repetitions = 5) - } - } -} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt index b4c75a6d1165..e8a61e8a1dae 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt @@ -20,25 +20,24 @@ import android.app.Instrumentation import android.content.Intent import android.platform.test.annotations.Presubmit import android.view.Surface -import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.FlickerBuilderProvider import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.entireScreenCovered import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.isRotated import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen -import com.android.server.wm.flicker.navBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.navBarLayerIsVisible import com.android.server.wm.flicker.navBarLayerRotatesAndScales -import com.android.server.wm.flicker.navBarWindowIsAlwaysVisible -import com.android.server.wm.flicker.noUncoveredRegions +import com.android.server.wm.flicker.navBarWindowIsVisible import com.android.server.wm.flicker.repetitions import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import com.android.server.wm.flicker.startRotation -import com.android.server.wm.flicker.statusBarLayerIsAlwaysVisible +import com.android.server.wm.flicker.statusBarLayerIsVisible import com.android.server.wm.flicker.statusBarLayerRotatesScales -import com.android.server.wm.flicker.statusBarWindowIsAlwaysVisible +import com.android.server.wm.flicker.statusBarWindowIsVisible import com.android.wm.shell.flicker.helpers.PipAppHelper import com.android.wm.shell.flicker.testapp.Components import org.junit.Test @@ -162,32 +161,29 @@ abstract class PipTransition(protected val testSpec: FlickerTestParameter) { @Presubmit @Test - open fun navBarWindowIsAlwaysVisible() = testSpec.navBarWindowIsAlwaysVisible() + open fun navBarWindowIsVisible() = testSpec.navBarWindowIsVisible() @Presubmit @Test - open fun statusBarWindowIsAlwaysVisible() = testSpec.statusBarWindowIsAlwaysVisible() + open fun statusBarWindowIsVisible() = testSpec.statusBarWindowIsVisible() - @FlakyTest + @Presubmit @Test - open fun navBarLayerIsAlwaysVisible() = testSpec.navBarLayerIsAlwaysVisible() + open fun navBarLayerIsVisible() = testSpec.navBarLayerIsVisible() - @FlakyTest + @Presubmit @Test - open fun statusBarLayerIsAlwaysVisible() = testSpec.statusBarLayerIsAlwaysVisible() + open fun statusBarLayerIsVisible() = testSpec.statusBarLayerIsVisible() @Presubmit @Test - open fun navBarLayerRotatesAndScales() = - testSpec.navBarLayerRotatesAndScales(testSpec.config.startRotation, Surface.ROTATION_0) + open fun navBarLayerRotatesAndScales() = testSpec.navBarLayerRotatesAndScales() @Presubmit @Test - open fun statusBarLayerRotatesScales() = - testSpec.statusBarLayerRotatesScales(testSpec.config.startRotation, Surface.ROTATION_0) + open fun statusBarLayerRotatesScales() = testSpec.statusBarLayerRotatesScales() @Presubmit @Test - open fun noUncoveredRegions() = - testSpec.noUncoveredRegions(testSpec.config.startRotation, Surface.ROTATION_0) + open fun entireScreenCovered() = testSpec.entireScreenCovered() }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt index 1f58bb2bf9db..d6dbc366aec0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt @@ -16,14 +16,13 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.FlickerTestParameterFactory -import com.android.server.wm.flicker.annotation.Group3 +import com.android.server.wm.flicker.annotation.Group4 import com.android.server.wm.flicker.dsl.FlickerBuilder import com.android.server.wm.flicker.helpers.WindowUtils import com.android.wm.shell.flicker.pip.PipTransition.BroadcastActionTrigger.Companion.ORIENTATION_LANDSCAPE @@ -44,7 +43,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -@Group3 +@Group4 class SetRequestedOrientationWhilePinnedTest( testSpec: FlickerTestParameter ) : PipTransition(testSpec) { @@ -83,55 +82,69 @@ class SetRequestedOrientationWhilePinnedTest( @FlakyTest @Test + override fun navBarLayerIsVisible() = super.navBarLayerIsVisible() + + @FlakyTest + @Test + override fun navBarWindowIsVisible() = super.navBarWindowIsVisible() + + @FlakyTest + @Test + override fun statusBarLayerIsVisible() = super.statusBarLayerIsVisible() + + @FlakyTest + @Test + override fun statusBarWindowIsVisible() = super.statusBarWindowIsVisible() + + @FlakyTest + @Test override fun navBarLayerRotatesAndScales() = super.navBarLayerRotatesAndScales() @FlakyTest @Test override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales() - @Presubmit + @FlakyTest @Test fun pipWindowInsideDisplay() { testSpec.assertWmStart { - frameRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + frameRegion(pipApp.component).coversAtMost(startingBounds) } } - @Presubmit + @FlakyTest @Test fun pipAppShowsOnTop() { testSpec.assertWmEnd { - showsAppWindowOnTop(pipApp.defaultWindowName) + isAppWindowOnTop(pipApp.component) } } - @Presubmit + @FlakyTest @Test fun pipLayerInsideDisplay() { testSpec.assertLayersStart { - visibleRegion(pipApp.defaultWindowName).coversAtMost(startingBounds) + visibleRegion(pipApp.component).coversAtMost(startingBounds) } } - @Presubmit + @FlakyTest @Test fun pipAlwaysVisible() = testSpec.assertWm { - this.showsAppWindow(pipApp.windowName) + this.isAppWindowVisible(pipApp.component) } - @Presubmit + @FlakyTest @Test fun pipAppLayerCoversFullScreen() { testSpec.assertLayersEnd { - visibleRegion(pipApp.defaultWindowName).coversExactly(endingBounds) + visibleRegion(pipApp.component).coversExactly(endingBounds) } } @FlakyTest @Test - override fun noUncoveredRegions() { - super.noUncoveredRegions() - } + override fun entireScreenCovered() = super.entireScreenCovered() companion object { @Parameterized.Parameters(name = "{0}") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 0110ba3f5b30..061218a015e4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -37,14 +37,17 @@ class TvPipMenuTests : TvPipTestBase() { private val systemUiResources = packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) private val pipBoundsWhileInMenu: Rect = systemUiResources.run { - val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME)) + val bounds = getString(getIdentifier("pip_menu_bounds", "string", + SYSTEM_UI_PACKAGE_NAME)) Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") } private val playButtonDescription = systemUiResources.run { - getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME)) + getString(getIdentifier("pip_play", "string", + SYSTEM_UI_PACKAGE_NAME)) } private val pauseButtonDescription = systemUiResources.run { - getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME)) + getString(getIdentifier("pip_pause", "string", + SYSTEM_UI_PACKAGE_NAME)) } @Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt index 1b73920046dc..1c663409b913 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt @@ -70,7 +70,8 @@ fun UiDevice.waitForTvPipMenuElementWithDescription(desc: String): UiObject2? { // descendant and then retrieve the element from the menu and return to the caller of this // method. val elementSelector = By.desc(desc) - val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR).hasDescendant(elementSelector) + val menuContainingElementSelector = By.copy(TV_PIP_MENU_SELECTOR) + .hasDescendant(elementSelector) return wait(Until.findObject(menuContainingElementSelector), WAIT_TIME_MS) ?.findObject(elementSelector) @@ -94,7 +95,8 @@ fun UiDevice.clickTvPipMenuFullscreenButton() { } fun UiDevice.clickTvPipMenuElementWithDescription(desc: String) { - focusOnAndClickTvPipMenuElement(By.desc(desc).pkg(SYSTEM_UI_PACKAGE_NAME)) || + focusOnAndClickTvPipMenuElement(By.desc(desc) + .pkg(SYSTEM_UI_PACKAGE_NAME)) || error("Could not focus on the Pip menu object with \"$desc\" description") // So apparently Accessibility framework on TV is not very reliable and sometimes the state of // the tree of accessibility nodes as seen by the accessibility clients kind of lags behind of diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml index 5549330df766..2cdbffa7589c 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/AndroidManifest.xml @@ -107,5 +107,20 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity + android:name=".LaunchBubbleActivity" + android:label="LaunchBubbleApp" + android:exported="true" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <action android:name="android.intent.action.VIEW" /> + </intent-filter> + </activity> + <activity + android:name=".BubbleActivity" + android:label="BubbleApp" + android:exported="false" + android:resizeableActivity="true" /> </application> </manifest> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png Binary files differnew file mode 100644 index 000000000000..d424a17b4157 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/bg.png diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml new file mode 100644 index 000000000000..b43f31da748d --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_bubble.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/> + <path + android:fillColor="#FF000000" + android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/> + <path + android:fillColor="#FF000000" + android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/> +</vector> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml new file mode 100644 index 000000000000..0e8c7a0fe64a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/drawable/ic_message.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,4c-4.97,0 -9,3.58 -9,8c0,1.53 0.49,2.97 1.33,4.18c0.12,0.18 0.2,0.46 0.1,0.66c-0.33,0.68 -0.79,1.52 -1.38,2.39c-0.12,0.17 0.01,0.41 0.21,0.39c0.63,-0.05 1.86,-0.26 3.38,-0.91c0.17,-0.07 0.36,-0.06 0.52,0.03C8.55,19.54 10.21,20 12,20c4.97,0 9,-3.58 9,-8S16.97,4 12,4zM16.94,11.63l-3.29,3.29c-0.13,0.13 -0.34,0.04 -0.34,-0.14v-1.57c0,-0.11 -0.1,-0.21 -0.21,-0.2c-2.19,0.06 -3.65,0.65 -5.14,1.95c-0.15,0.13 -0.38,0 -0.33,-0.19c0.7,-2.57 2.9,-4.57 5.5,-4.75c0.1,-0.01 0.18,-0.09 0.18,-0.19V8.2c0,-0.18 0.22,-0.27 0.34,-0.14l3.29,3.29C17.02,11.43 17.02,11.55 16.94,11.63z" + android:fillColor="#000000" + android:fillType="evenOdd"/> +</vector> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml new file mode 100644 index 000000000000..f8b0ca3da26e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_bubble.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <Button + android:id="@+id/button_finish" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:layout_marginStart="8dp" + android:text="Finish" /> + <Button + android:id="@+id/button_new_task" + android:layout_width="wrap_content" + android:layout_height="46dp" + android:layout_marginStart="8dp" + android:text="New Task" /> + <Button + android:id="@+id/button_new_bubble" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:text="New Bubble" /> + + <Button + android:id="@+id/button_activity_for_result" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_marginStart="8dp" + android:text="Activity For Result" /> +</LinearLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml new file mode 100644 index 000000000000..f23c46455c63 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/res/layout/activity_main.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2021 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. +--> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/black"> + + <Button + android:id="@+id/button_create" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="Add Bubble" /> + + <Button + android:id="@+id/button_cancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/button_create" + android:layout_centerHorizontal="true" + android:layout_marginTop="20dp" + android:text="Cancel Bubble" /> + + <Button + android:id="@+id/button_cancel_all" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/button_cancel" + android:layout_centerHorizontal="true" + android:layout_marginTop="20dp" + android:text="Cancel All Bubble" /> +</RelativeLayout> diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java new file mode 100644 index 000000000000..bc3bc75ab903 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleActivity.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 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.testapp; + + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +public class BubbleActivity extends Activity { + private int mNotifId = 0; + + public BubbleActivity() { + super(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (intent != null) { + mNotifId = intent.getIntExtra(BubbleHelper.EXTRA_BUBBLE_NOTIF_ID, -1); + } else { + mNotifId = -1; + } + + setContentView(R.layout.activity_bubble); + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + String result = resultCode == Activity.RESULT_OK ? "OK" : "CANCELLED"; + Toast.makeText(this, "Activity result: " + result, Toast.LENGTH_SHORT).show(); + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java new file mode 100644 index 000000000000..d743dffd3c9e --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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.testapp; + + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Person; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.graphics.drawable.Icon; +import android.os.SystemClock; +import android.service.notification.StatusBarNotification; +import android.view.WindowManager; + +import java.util.HashMap; + +public class BubbleHelper { + + static final String EXTRA_BUBBLE_NOTIF_ID = "EXTRA_BUBBLE_NOTIF_ID"; + static final String CHANNEL_ID = "bubbles"; + static final String CHANNEL_NAME = "Bubbles"; + static final int DEFAULT_HEIGHT_DP = 300; + + private static BubbleHelper sInstance; + + private final Context mContext; + private NotificationManager mNotificationManager; + private float mDisplayHeight; + + private HashMap<Integer, BubbleInfo> mBubbleMap = new HashMap<>(); + + private int mNextNotifyId = 0; + private int mColourIndex = 0; + + public static class BubbleInfo { + public int id; + public int height; + public Icon icon; + + public BubbleInfo(int id, int height, Icon icon) { + this.id = id; + this.height = height; + this.icon = icon; + } + } + + public static BubbleHelper getInstance(Context context) { + if (sInstance == null) { + sInstance = new BubbleHelper(context); + } + return sInstance; + } + + private BubbleHelper(Context context) { + mContext = context; + mNotificationManager = context.getSystemService(NotificationManager.class); + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription("Channel that posts bubbles"); + channel.setAllowBubbles(true); + mNotificationManager.createNotificationChannel(channel); + + Point p = new Point(); + WindowManager wm = context.getSystemService(WindowManager.class); + wm.getDefaultDisplay().getRealSize(p); + mDisplayHeight = p.y; + + } + + private int getNextNotifyId() { + int id = mNextNotifyId; + mNextNotifyId++; + return id; + } + + private Icon getIcon() { + return Icon.createWithResource(mContext, R.drawable.bg); + } + + public int addNewBubble(boolean autoExpand, boolean suppressNotif) { + int id = getNextNotifyId(); + BubbleInfo info = new BubbleInfo(id, DEFAULT_HEIGHT_DP, getIcon()); + mBubbleMap.put(info.id, info); + + Notification.BubbleMetadata data = getBubbleBuilder(info) + .setSuppressNotification(suppressNotif) + .setAutoExpandBubble(false) + .build(); + Notification notification = getNotificationBuilder(info.id) + .setBubbleMetadata(data).build(); + + mNotificationManager.notify(info.id, notification); + return info.id; + } + + private Notification.Builder getNotificationBuilder(int id) { + Person chatBot = new Person.Builder() + .setBot(true) + .setName("BubbleBot") + .setImportant(true) + .build(); + + RemoteInput remoteInput = new RemoteInput.Builder("key") + .setLabel("Reply") + .build(); + + String shortcutId = "BubbleChat"; + return new Notification.Builder(mContext, CHANNEL_ID) + .setChannelId(CHANNEL_ID) + .setShortcutId(shortcutId) + .setContentIntent(PendingIntent.getActivity(mContext, 0, + new Intent(mContext, LaunchBubbleActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT)) + .setStyle(new Notification.MessagingStyle(chatBot) + .setConversationTitle("Bubble Chat") + .addMessage("Hello? This is bubble: " + id, + SystemClock.currentThreadTimeMillis() - 300000, chatBot) + .addMessage("Is it me, " + id + ", you're looking for?", + SystemClock.currentThreadTimeMillis(), chatBot) + ) + .setSmallIcon(R.drawable.ic_bubble); + } + + private Notification.BubbleMetadata.Builder getBubbleBuilder(BubbleInfo info) { + Intent target = new Intent(mContext, BubbleActivity.class); + target.putExtra(EXTRA_BUBBLE_NOTIF_ID, info.id); + PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, info.id, target, + PendingIntent.FLAG_UPDATE_CURRENT); + + return new Notification.BubbleMetadata.Builder() + .setIntent(bubbleIntent) + .setIcon(info.icon) + .setDesiredHeight(info.height); + } + + public void cancel(int id) { + mNotificationManager.cancel(id); + } + + public void cancelAll() { + mNotificationManager.cancelAll(); + } + + public void cancelLast() { + StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications(); + if (activeNotifications.length > 0) { + mNotificationManager.cancel( + activeNotifications[activeNotifications.length - 1].getId()); + } + } + + public void cancelFirst() { + StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications(); + if (activeNotifications.length > 0) { + mNotificationManager.cancel(activeNotifications[0].getId()); + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java index 0ead91bb37de..0ed59bdafd1d 100644 --- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/Components.java @@ -87,4 +87,16 @@ public class Components { public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".SplitScreenSecondaryActivity"); } + + public static class LaunchBubbleActivity { + public static final String LABEL = "LaunchBubbleApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".LaunchBubbleActivity"); + } + + public static class BubbleActivity { + public static final String LABEL = "BubbleApp"; + public static final ComponentName COMPONENT = new ComponentName(PACKAGE_NAME, + PACKAGE_NAME + ".BubbleActivity"); + } } diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java new file mode 100644 index 000000000000..71fa66d8a61c --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/LaunchBubbleActivity.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 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.testapp; + + +import android.app.Activity; +import android.app.Person; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.view.View; + +import java.util.Arrays; + +public class LaunchBubbleActivity extends Activity { + + private BubbleHelper mBubbleHelper; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addInboxShortcut(getApplicationContext()); + mBubbleHelper = BubbleHelper.getInstance(this); + setContentView(R.layout.activity_main); + findViewById(R.id.button_create).setOnClickListener(this::add); + findViewById(R.id.button_cancel).setOnClickListener(this::cancel); + findViewById(R.id.button_cancel_all).setOnClickListener(this::cancelAll); + } + + private void add(View v) { + mBubbleHelper.addNewBubble(false /* autoExpand */, false /* suppressNotif */); + } + + private void cancel(View v) { + mBubbleHelper.cancelLast(); + } + + private void cancelAll(View v) { + mBubbleHelper.cancelAll(); + } + + private void addInboxShortcut(Context context) { + Icon icon = Icon.createWithResource(this, R.drawable.bg); + Person[] persons = new Person[4]; + for (int i = 0; i < persons.length; i++) { + persons[i] = new Person.Builder() + .setBot(false) + .setIcon(icon) + .setName("google" + i) + .setImportant(true) + .build(); + } + + ShortcutInfo shortcut = new ShortcutInfo.Builder(context, "BubbleChat") + .setShortLabel("BubbleChat") + .setLongLived(true) + .setIntent(new Intent(Intent.ACTION_VIEW)) + .setIcon(Icon.createWithResource(context, R.drawable.ic_message)) + .setPersons(persons) + .build(); + ShortcutManager scmanager = context.getSystemService(ShortcutManager.class); + scmanager.addDynamicShortcuts(Arrays.asList(shortcut)); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 6b74b620dad7..a3b98a8fc880 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -56,7 +56,7 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; -import com.android.wm.shell.sizecompatui.SizeCompatUIController; +import com.android.wm.shell.compatui.CompatUIController; import org.junit.Before; import org.junit.Test; @@ -65,6 +65,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Optional; /** * Tests for the shell task organizer. @@ -81,7 +82,7 @@ public class ShellTaskOrganizerTests { @Mock private Context mContext; @Mock - private SizeCompatUIController mSizeCompatUI; + private CompatUIController mCompatUI; ShellTaskOrganizer mOrganizer; private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); @@ -131,7 +132,7 @@ public class ShellTaskOrganizerTests { .when(mTaskOrganizerController).registerTaskOrganizer(any()); } catch (RemoteException e) {} mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext, - mSizeCompatUI)); + mCompatUI, Optional.empty())); } @Test @@ -197,6 +198,43 @@ public class ShellTaskOrganizerTests { } @Test + public void testAddListenerForMultipleTypes() { + RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo2, null); + + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, + TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_FULLSCREEN); + + // onTaskAppeared event should be delivered once for each taskInfo. + assertTrue(listener.appeared.contains(taskInfo1)); + assertTrue(listener.appeared.contains(taskInfo2)); + assertEquals(2, listener.appeared.size()); + } + + @Test + public void testRemoveListenerForMultipleTypes() { + RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN); + mOrganizer.onTaskAppeared(taskInfo1, null); + RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW); + mOrganizer.onTaskAppeared(taskInfo2, null); + + TrackingTaskListener listener = new TrackingTaskListener(); + mOrganizer.addListenerForType(listener, + TASK_LISTENER_TYPE_MULTI_WINDOW, TASK_LISTENER_TYPE_FULLSCREEN); + + mOrganizer.removeListener(listener); + + // If listener is removed properly, onTaskInfoChanged event shouldn't be delivered. + mOrganizer.onTaskInfoChanged(taskInfo1); + assertTrue(listener.infoChanged.isEmpty()); + mOrganizer.onTaskInfoChanged(taskInfo2); + assertTrue(listener.infoChanged.isEmpty()); + } + + @Test public void testWindowingModeChange() { RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW); TrackingTaskListener mwListener = new TrackingTaskListener(); @@ -296,34 +334,34 @@ public class ShellTaskOrganizerTests { mOrganizer.onTaskAppeared(taskInfo1, null); // sizeCompatActivity is null if top activity is not in size compat. - verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, null /* taskConfig */, null /* taskListener */); // sizeCompatActivity is non-null if top activity is in size compat. - clearInvocations(mSizeCompatUI); + clearInvocations(mCompatUI); final RunningTaskInfo taskInfo2 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo2.displayId = taskInfo1.displayId; taskInfo2.topActivityInSizeCompat = true; taskInfo2.isVisible = true; mOrganizer.onTaskInfoChanged(taskInfo2); - verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, taskInfo1.configuration, taskListener); // Not show size compat UI if task is not visible. - clearInvocations(mSizeCompatUI); + clearInvocations(mCompatUI); final RunningTaskInfo taskInfo3 = createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode()); taskInfo3.displayId = taskInfo1.displayId; taskInfo3.topActivityInSizeCompat = true; taskInfo3.isVisible = false; mOrganizer.onTaskInfoChanged(taskInfo3); - verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, null /* taskConfig */, null /* taskListener */); - clearInvocations(mSizeCompatUI); + clearInvocations(mCompatUI); mOrganizer.onTaskVanished(taskInfo1); - verify(mSizeCompatUI).onSizeCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, + verify(mCompatUI).onCompatInfoChanged(taskInfo1.displayId, taskInfo1.taskId, null /* taskConfig */, null /* taskListener */); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java index 5bdf831a81f4..6080f3ae78e8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java @@ -26,6 +26,7 @@ import androidx.test.InstrumentationRegistry; import org.junit.After; import org.junit.Before; +import org.mockito.MockitoAnnotations; /** * Base class that does shell test case setup. @@ -36,6 +37,7 @@ public abstract class ShellTestCase { @Before public void shellSetup() { + MockitoAnnotations.initMocks(this); final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); final DisplayManager dm = context.getSystemService(DisplayManager.class); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java index 20ac5bf8fa84..1cbad155ba7b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java @@ -47,6 +47,8 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.wm.shell.common.HandlerExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; import org.junit.After; import org.junit.Before; @@ -71,6 +73,8 @@ public class TaskViewTest extends ShellTestCase { ShellTaskOrganizer mOrganizer; @Mock HandlerExecutor mExecutor; + @Mock + SyncTransactionQueue mSyncQueue; SurfaceSession mSession; SurfaceControl mLeash; @@ -99,7 +103,14 @@ public class TaskViewTest extends ShellTestCase { }).when(mExecutor).execute(any()); when(mOrganizer.getExecutor()).thenReturn(mExecutor); - mTaskView = new TaskView(mContext, mOrganizer); + + doAnswer((InvocationOnMock invocationOnMock) -> { + final TransactionRunnable r = invocationOnMock.getArgument(0); + r.runWithTransaction(new SurfaceControl.Transaction()); + return null; + }).when(mSyncQueue).runInSync(any()); + + mTaskView = new TaskView(mContext, mOrganizer, mSyncQueue); mTaskView.setListener(mExecutor, mViewListener); } @@ -112,7 +123,7 @@ public class TaskViewTest extends ShellTestCase { @Test public void testSetPendingListener_throwsException() { - TaskView taskView = new TaskView(mContext, mOrganizer); + TaskView taskView = new TaskView(mContext, mOrganizer, mSyncQueue); taskView.setListener(mExecutor, mViewListener); try { taskView.setListener(mExecutor, mViewListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java index 27c626170a4b..294bc1276291 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/apppairs/TestAppPairsController.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.mock; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; @@ -30,7 +31,7 @@ public class TestAppPairsController extends AppPairsController { public TestAppPairsController(ShellTaskOrganizer organizer, SyncTransactionQueue syncQueue, DisplayController displayController) { super(organizer, syncQueue, displayController, mock(ShellExecutor.class), - mock(DisplayImeController.class)); + mock(DisplayImeController.class), mock(DisplayInsetsController.class)); mPool = new TestAppPairsPool(this); setPairsPool(mPool); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 3e3195fe8dc5..8bc1223cfd64 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -39,6 +39,7 @@ import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.util.Log; import android.util.Pair; import android.view.WindowManager; @@ -131,7 +132,7 @@ public class BubbleDataTest extends ShellTestCase { NotificationListenerService.Ranking ranking = mock(NotificationListenerService.Ranking.class); - when(ranking.visuallyInterruptive()).thenReturn(true); + when(ranking.isTextChanged()).thenReturn(true); mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking); mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null, mMainExecutor); @@ -793,7 +794,7 @@ public class BubbleDataTest extends ShellTestCase { } @Test - public void test_expanded_removeLastBubble_collapsesStack() { + public void test_expanded_removeLastBubble_showsOverflowIfNotEmpty() { // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); changeExpandedStateAtTime(true, 2000); @@ -802,6 +803,21 @@ public class BubbleDataTest extends ShellTestCase { // Test mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE); verifyUpdateReceived(); + assertThat(mBubbleData.getOverflowBubbles().size()).isGreaterThan(0); + assertSelectionChangedTo(mBubbleData.getOverflow()); + } + + @Test + public void test_expanded_removeLastBubble_collapsesIfOverflowEmpty() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + changeExpandedStateAtTime(true, 2000); + mBubbleData.setListener(mListener); + + // Test + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NO_BUBBLE_UP); + verifyUpdateReceived(); + assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); assertExpandedChangedTo(false); } @@ -869,6 +885,60 @@ public class BubbleDataTest extends ShellTestCase { assertNotNull(mBubbleData.getOverflowBubbleWithKey(mBubbleA2.getKey())); } + /** + * Verifies that after the stack is collapsed with the overflow selected, it will select + * the top bubble upon next expansion. + */ + @Test + public void test_collapseWithOverflowSelected_nextExpansion() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setExpanded(true); + + mBubbleData.setListener(mListener); + + // Select the overflow + mBubbleData.setShowingOverflow(true); + mBubbleData.setSelectedBubble(mBubbleData.getOverflow()); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleData.getOverflow()); + + // Collapse + mBubbleData.setExpanded(false); + verifyUpdateReceived(); + assertSelectionNotChanged(); + + // Expand (here we should select the new bubble) + mBubbleData.setExpanded(true); + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA2); + } + + /** + * - have a maxed out bubble stack & all of the bubbles have been recently accessed + * - bubble a notification that was posted before any of those bubbles were accessed + * => that bubble should be added + * + */ + @Test + public void test_addOldNotifWithNewerBubbles() { + sendUpdatedEntryAtTime(mEntryA1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryA3, 4000); + sendUpdatedEntryAtTime(mEntryB1, 5000); + sendUpdatedEntryAtTime(mEntryB2, 6000); + + mBubbleData.setListener(mListener); + sendUpdatedEntryAtTime(mEntryB3, 1000 /* postTime */, 7000 /* currentTime */); + verifyUpdateReceived(); + + // B3 is in the stack + assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleB3.getKey())).isNotNull(); + // A1 is the oldest so it's in the overflow + assertThat(mBubbleData.getOverflowBubbleWithKey(mEntryA1.getKey())).isNotNull(); + assertOrderChangedTo(mBubbleB3, mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA2); + } + private void verifyUpdateReceived() { verify(mListener).applyUpdate(mUpdateCaptor.capture()); reset(mListener); @@ -902,7 +972,7 @@ public class BubbleDataTest extends ShellTestCase { assertWithMessage("selectionChanged").that(update.selectionChanged).isFalse(); } - private void assertSelectionChangedTo(Bubble bubble) { + private void assertSelectionChangedTo(BubbleViewProvider bubble) { BubbleData.Update update = mUpdateCaptor.getValue(); assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue(); assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); @@ -925,7 +995,6 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.overflowBubbles).isEqualTo(bubbles); } - private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName, NotificationListenerService.Ranking ranking) { return createBubbleEntry(userId, notifKey, packageName, ranking, 1000); @@ -971,15 +1040,21 @@ public class BubbleDataTest extends ShellTestCase { } private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime) { - sendUpdatedEntryAtTime(entry, postTime, true /* visuallyInterruptive */); + setCurrentTime(postTime); + sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */); + } + + private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, long currentTime) { + setCurrentTime(currentTime); + sendUpdatedEntryAtTime(entry, postTime, true /* isTextChanged */); } private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime, - boolean visuallyInterruptive) { + boolean textChanged) { setPostTime(entry, postTime); // BubbleController calls this: Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */); - b.setVisuallyInterruptiveForTest(visuallyInterruptive); + b.setTextChangedForTest(textChanged); // And then this mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/, true /* showInShade */); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java index 6644eaf28a62..5c1bcb9753a4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleFlyoutViewTest.java @@ -63,7 +63,7 @@ public class BubbleFlyoutViewTest extends ShellTestCase { mFlyoutMessage.senderName = "Josh"; mFlyoutMessage.message = "Hello"; - mFlyout = new BubbleFlyoutView(getContext()); + mFlyout = new BubbleFlyoutView(getContext(), mPositioner); mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text); mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name); @@ -75,9 +75,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase { public void testShowFlyout_isVisible() { mFlyout.setupFlyoutStartingAsDot( mFlyoutMessage, - new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter, - false, - mPositioner); + new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter, + false); mFlyout.setVisibility(View.VISIBLE); assertEquals("Hello", mFlyoutText.getText()); @@ -89,9 +88,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase { public void testFlyoutHide_runsCallback() { Runnable after = mock(Runnable.class); mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage, - new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter, - false, - mPositioner); + new PointF(100, 100), true, Color.WHITE, null, after, mDotCenter, + false); mFlyout.hideFlyout(); verify(after).run(); @@ -100,9 +98,8 @@ public class BubbleFlyoutViewTest extends ShellTestCase { @Test public void testSetCollapsePercent() { mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage, - new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter, - false, - mPositioner); + new PointF(100, 100), true, Color.WHITE, null, null, mDotCenter, + false); mFlyout.setVisibility(View.VISIBLE); mFlyout.setCollapsePercent(1f); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java index 1eba3c266358..335222e98c6c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java @@ -16,9 +16,12 @@ package com.android.wm.shell.bubbles.animation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.SuppressLint; import android.content.res.Configuration; @@ -36,12 +39,12 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.bubbles.BubbleStackView; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Spy; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -49,26 +52,32 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC private int mDisplayWidth = 500; private int mDisplayHeight = 1000; - private int mExpandedViewPadding = 10; private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class); - @Spy ExpandedAnimationController mExpandedController; private int mStackOffset; private PointF mExpansionPoint; + private BubblePositioner mPositioner; + private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState(); @SuppressLint("VisibleForTests") @Before public void setUp() throws Exception { super.setUp(); - BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class)); - positioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, + mPositioner = new BubblePositioner(getContext(), mock(WindowManager.class)); + mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT, Insets.of(0, 0, 0, 0), new Rect(0, 0, mDisplayWidth, mDisplayHeight)); - mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding, - mOnBubbleAnimatedOutAction); + + BubbleStackView stackView = mock(BubbleStackView.class); + when(stackView.getState()).thenReturn(getStackViewState()); + + mExpandedController = new ExpandedAnimationController(mPositioner, + mOnBubbleAnimatedOutAction, + stackView); + spyOn(mExpandedController); addOneMoreThanBubbleLimitBubbles(); mLayout.setActiveController(mExpandedController); @@ -78,6 +87,13 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC mExpansionPoint = new PointF(100, 100); } + public BubbleStackView.StackViewState getStackViewState() { + mStackViewState.numberOfBubbles = mLayout.getChildCount(); + mStackViewState.selectedIndex = 0; + mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint); + return mStackViewState; + } + @Test @Ignore public void testExpansionAndCollapse() throws InterruptedException { @@ -121,6 +137,12 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC testBubblesInCorrectExpandedPositions(); } + @Test + public void testDragBubbleOutDoesntNPE() throws InterruptedException { + mExpandedController.onGestureFinished(); + mExpandedController.dragBubbleOut(mViews.get(0), 1, 1); + } + /** Expand the stack and wait for animations to finish. */ private void expand() throws InterruptedException { mExpandedController.expandFromStack(mock(Runnable.class)); @@ -143,11 +165,12 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC private void testBubblesInCorrectExpandedPositions() { // Check all the visible bubbles to see if they're in the right place. for (int i = 0; i < mLayout.getChildCount(); i++) { - float expectedPosition = mExpandedController.getBubbleXOrYForOrientation(i); - assertEquals(expectedPosition, + PointF expectedPosition = mPositioner.getExpandedBubbleXY(i, + getStackViewState()); + assertEquals(expectedPosition.x, mLayout.getChildAt(i).getTranslationX(), 2f); - assertEquals(expectedPosition, + assertEquals(expectedPosition.y, mLayout.getChildAt(i).getTranslationY(), 2f); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java index ef046d48e1cf..b88845044263 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -58,7 +58,7 @@ public class DisplayImeControllerTest { mT = mock(SurfaceControl.Transaction.class); mMock = mock(IInputMethodManager.class); mExecutor = spy(Runnable::run); - mPerDisplay = new DisplayImeController(null, null, mExecutor, new TransactionPool() { + mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() { @Override public SurfaceControl.Transaction acquire() { return mT; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java new file mode 100644 index 000000000000..b66c2b4aee9b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.os.RemoteException; +import android.util.SparseArray; +import android.view.IDisplayWindowInsetsController; +import android.view.IWindowManager; +import android.view.InsetsSourceControl; +import android.view.InsetsState; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +@SmallTest +public class DisplayInsetsControllerTest { + + private static final int SECOND_DISPLAY = DEFAULT_DISPLAY + 10; + + @Mock + private IWindowManager mWm; + @Mock + private DisplayController mDisplayController; + private DisplayInsetsController mController; + private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId; + private TestShellExecutor mExecutor; + + private ArgumentCaptor<Integer> mDisplayIdCaptor; + private ArgumentCaptor<IDisplayWindowInsetsController> mInsetsControllerCaptor; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mExecutor = new TestShellExecutor(); + mInsetsControllersByDisplayId = new SparseArray<>(); + mDisplayIdCaptor = ArgumentCaptor.forClass(Integer.class); + mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class); + mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor); + addDisplay(DEFAULT_DISPLAY); + } + + @Test + public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService() + throws RemoteException { + addDisplay(SECOND_DISPLAY); + + verify(mWm).setDisplayWindowInsetsController(eq(SECOND_DISPLAY), notNull()); + } + + @Test + public void testOnDisplayRemoved_unsetsDisplayWindowInsetsControllerInWMService() + throws RemoteException { + addDisplay(SECOND_DISPLAY); + removeDisplay(SECOND_DISPLAY); + + verify(mWm).setDisplayWindowInsetsController(SECOND_DISPLAY, null); + } + + @Test + public void testPerDisplayListenerCallback() throws RemoteException { + TrackedListener defaultListener = new TrackedListener(); + TrackedListener secondListener = new TrackedListener(); + addDisplay(SECOND_DISPLAY); + mController.addInsetsChangedListener(DEFAULT_DISPLAY, defaultListener); + mController.addInsetsChangedListener(SECOND_DISPLAY, secondListener); + + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).topFocusedWindowChanged(null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).insetsControlChanged(null, null); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).showInsets(0, false); + mInsetsControllersByDisplayId.get(DEFAULT_DISPLAY).hideInsets(0, false); + mExecutor.flushAll(); + + assertTrue(defaultListener.topFocusedWindowChangedCount == 1); + assertTrue(defaultListener.insetsChangedCount == 1); + assertTrue(defaultListener.insetsControlChangedCount == 1); + assertTrue(defaultListener.showInsetsCount == 1); + assertTrue(defaultListener.hideInsetsCount == 1); + + assertTrue(secondListener.topFocusedWindowChangedCount == 0); + assertTrue(secondListener.insetsChangedCount == 0); + assertTrue(secondListener.insetsControlChangedCount == 0); + assertTrue(secondListener.showInsetsCount == 0); + assertTrue(secondListener.hideInsetsCount == 0); + + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).topFocusedWindowChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsChanged(null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).insetsControlChanged(null, null); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).showInsets(0, false); + mInsetsControllersByDisplayId.get(SECOND_DISPLAY).hideInsets(0, false); + mExecutor.flushAll(); + + assertTrue(defaultListener.topFocusedWindowChangedCount == 1); + assertTrue(defaultListener.insetsChangedCount == 1); + assertTrue(defaultListener.insetsControlChangedCount == 1); + assertTrue(defaultListener.showInsetsCount == 1); + assertTrue(defaultListener.hideInsetsCount == 1); + + assertTrue(secondListener.topFocusedWindowChangedCount == 1); + assertTrue(secondListener.insetsChangedCount == 1); + assertTrue(secondListener.insetsControlChangedCount == 1); + assertTrue(secondListener.showInsetsCount == 1); + assertTrue(secondListener.hideInsetsCount == 1); + } + + private void addDisplay(int displayId) throws RemoteException { + mController.onDisplayAdded(displayId); + verify(mWm, times(mInsetsControllersByDisplayId.size() + 1)) + .setDisplayWindowInsetsController(mDisplayIdCaptor.capture(), + mInsetsControllerCaptor.capture()); + List<Integer> displayIds = mDisplayIdCaptor.getAllValues(); + List<IDisplayWindowInsetsController> insetsControllers = + mInsetsControllerCaptor.getAllValues(); + for (int i = 0; i < displayIds.size(); i++) { + mInsetsControllersByDisplayId.put(displayIds.get(i), insetsControllers.get(i)); + } + } + + private void removeDisplay(int displayId) { + mController.onDisplayRemoved(displayId); + mInsetsControllersByDisplayId.remove(displayId); + } + + private static class TrackedListener implements + DisplayInsetsController.OnInsetsChangedListener { + int topFocusedWindowChangedCount = 0; + int insetsChangedCount = 0; + int insetsControlChangedCount = 0; + int showInsetsCount = 0; + int hideInsetsCount = 0; + + @Override + public void topFocusedWindowChanged(String packageName) { + topFocusedWindowChangedCount++; + } + + @Override + public void insetsChanged(InsetsState insetsState) { + insetsChangedCount++; + } + + @Override + public void insetsControlChanged(InsetsState insetsState, + InsetsSourceControl[] activeControls) { + insetsControlChangedCount++; + } + + @Override + public void showInsets(int types, boolean fromIme) { + showInsetsCount++; + } + + @Override + public void hideInsets(int types, boolean fromIme) { + hideInsetsCount++; + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java index 88e754c58792..0ffa5b35331d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java @@ -21,9 +21,14 @@ import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.content.res.Resources; @@ -35,8 +40,12 @@ import android.view.DisplayInfo; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.policy.SystemBarUtils; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import org.mockito.MockitoSession; /** * Tests for {@link DisplayLayout}. @@ -46,29 +55,48 @@ import org.junit.Test; */ @SmallTest public class DisplayLayoutTest { + private MockitoSession mMockitoSession; + + @Before + public void setup() { + mMockitoSession = mockitoSession() + .initMocks(this) + .mockStatic(SystemBarUtils.class) + .startMocking(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } @Test public void testInsets() { - Resources res = createResources(40, 50, false, 30, 40); + Resources res = createResources(40, 50, false); // Test empty display, no bars or anything DisplayInfo info = createDisplayInfo(1000, 1500, 0, ROTATION_0); DisplayLayout dl = new DisplayLayout(info, res, false, false); + when(SystemBarUtils.getStatusBarHeight(eq(res), any())).thenReturn(40); + dl.recalcInsets(res); assertEquals(new Rect(0, 0, 0, 0), dl.stableInsets()); assertEquals(new Rect(0, 0, 0, 0), dl.nonDecorInsets()); // Test with bars dl = new DisplayLayout(info, res, true, true); + dl.recalcInsets(res); assertEquals(new Rect(0, 40, 0, 50), dl.stableInsets()); assertEquals(new Rect(0, 0, 0, 50), dl.nonDecorInsets()); // Test just cutout info = createDisplayInfo(1000, 1500, 60, ROTATION_0); dl = new DisplayLayout(info, res, false, false); + dl.recalcInsets(res); assertEquals(new Rect(0, 60, 0, 0), dl.stableInsets()); assertEquals(new Rect(0, 60, 0, 0), dl.nonDecorInsets()); // Test with bars and cutout dl = new DisplayLayout(info, res, true, true); + dl.recalcInsets(res); assertEquals(new Rect(0, 60, 0, 50), dl.stableInsets()); assertEquals(new Rect(0, 60, 0, 50), dl.nonDecorInsets()); } @@ -76,27 +104,30 @@ public class DisplayLayoutTest { @Test public void testRotate() { // Basic rotate utility - Resources res = createResources(40, 50, false, 30, 40); + Resources res = createResources(40, 50, false); DisplayInfo info = createDisplayInfo(1000, 1500, 60, ROTATION_0); DisplayLayout dl = new DisplayLayout(info, res, true, true); + when(SystemBarUtils.getStatusBarHeight(eq(res), any())).thenReturn(40); + dl.recalcInsets(res); assertEquals(new Rect(0, 60, 0, 50), dl.stableInsets()); assertEquals(new Rect(0, 60, 0, 50), dl.nonDecorInsets()); // Rotate to 90 + when(SystemBarUtils.getStatusBarHeight(eq(res), any())).thenReturn(30); dl.rotateTo(res, ROTATION_90); assertEquals(new Rect(60, 30, 0, 40), dl.stableInsets()); assertEquals(new Rect(60, 0, 0, 40), dl.nonDecorInsets()); // Rotate with moving navbar - res = createResources(40, 50, true, 30, 40); + res = createResources(40, 50, true); dl = new DisplayLayout(info, res, true, true); + when(SystemBarUtils.getStatusBarHeight(eq(res), any())).thenReturn(30); dl.rotateTo(res, ROTATION_270); assertEquals(new Rect(40, 30, 60, 0), dl.stableInsets()); assertEquals(new Rect(40, 0, 60, 0), dl.nonDecorInsets()); } - private Resources createResources( - int navLand, int navPort, boolean navMoves, int statusLand, int statusPort) { + private Resources createResources(int navLand, int navPort, boolean navMoves) { Configuration cfg = new Configuration(); cfg.uiMode = UI_MODE_TYPE_NORMAL; Resources res = mock(Resources.class); @@ -108,8 +139,6 @@ public class DisplayLayoutTest { doReturn(navPort).when(res).getDimensionPixelSize(R.dimen.navigation_bar_height); doReturn(navLand).when(res).getDimensionPixelSize(R.dimen.navigation_bar_width); doReturn(navMoves).when(res).getBoolean(R.bool.config_navBarCanMove); - doReturn(statusLand).when(res).getDimensionPixelSize(R.dimen.status_bar_height_landscape); - doReturn(statusPort).when(res).getDimensionPixelSize(R.dimen.status_bar_height_portrait); doReturn(cfg).when(res).getConfiguration(); return res; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index 952dc31cdaee..453050fcfab4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -24,11 +24,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.content.res.Configuration; import android.graphics.Rect; -import android.view.SurfaceControl; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -42,6 +42,8 @@ import com.android.wm.shell.common.DisplayImeController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -50,42 +52,63 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) public class SplitLayoutTests extends ShellTestCase { @Mock SplitLayout.SplitLayoutHandler mSplitLayoutHandler; - @Mock SurfaceControl mRootLeash; + @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks; @Mock DisplayImeController mDisplayImeController; @Mock ShellTaskOrganizer mTaskOrganizer; + @Captor ArgumentCaptor<Runnable> mRunnableCaptor; private SplitLayout mSplitLayout; @Before public void setup() { MockitoAnnotations.initMocks(this); - mSplitLayout = new SplitLayout( + mSplitLayout = spy(new SplitLayout( "TestSplitLayout", mContext, - getConfiguration(false), + getConfiguration(), mSplitLayoutHandler, - b -> b.setParent(mRootLeash), + mCallbacks, mDisplayImeController, - mTaskOrganizer); + mTaskOrganizer, + false /* applyDismissingParallax */)); } @Test @UiThreadTest public void testUpdateConfiguration() { - mSplitLayout.init(); - assertThat(mSplitLayout.updateConfiguration(getConfiguration(false))).isFalse(); - assertThat(mSplitLayout.updateConfiguration(getConfiguration(true))).isTrue(); + final Configuration config = getConfiguration(); + + // Verify it returns true if new config won't affect split layout. + assertThat(mSplitLayout.updateConfiguration(config)).isFalse(); + + // Verify updateConfiguration returns true if the orientation changed. + config.orientation = ORIENTATION_LANDSCAPE; + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if it rotated. + config.windowConfiguration.setRotation(1); + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); + + // Verify updateConfiguration returns true if the root bounds changed. + config.windowConfiguration.setBounds(new Rect(0, 0, 2160, 1080)); + assertThat(mSplitLayout.updateConfiguration(config)).isTrue(); } @Test public void testUpdateDivideBounds() { mSplitLayout.updateDivideBounds(anyInt()); - verify(mSplitLayoutHandler).onBoundsChanging(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class)); } @Test public void testSetDividePosition() { mSplitLayout.setDividePosition(anyInt()); - verify(mSplitLayoutHandler).onBoundsChanged(any(SplitLayout.class)); + verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class)); + } + + @Test + public void testSetDivideRatio() { + mSplitLayout.setDivideRatio(0.5f); + verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class)); } @Test @@ -96,24 +119,40 @@ public class SplitLayoutTests extends ShellTestCase { @Test @UiThreadTest - public void testSnapToDismissTarget() { + public void testSnapToDismissStart() { // verify it callbacks properly when the snap target indicates dismissing split. DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_START); + mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + waitDividerFlingFinished(); verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false)); - snapTarget = getSnapTarget(0 /* position */, + } + + @Test + @UiThreadTest + public void testSnapToDismissEnd() { + // verify it callbacks properly when the snap target indicates dismissing split. + DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */, DividerSnapAlgorithm.SnapTarget.FLAG_DISMISS_END); + mSplitLayout.snapToTarget(0 /* currentPosition */, snapTarget); + waitDividerFlingFinished(); verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true)); } - private static Configuration getConfiguration(boolean isLandscape) { + private void waitDividerFlingFinished() { + verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), mRunnableCaptor.capture()); + mRunnableCaptor.getValue().run(); + } + + private static Configuration getConfiguration() { final Configuration configuration = new Configuration(); configuration.unset(); - configuration.orientation = isLandscape ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; + configuration.orientation = ORIENTATION_PORTRAIT; + configuration.windowConfiguration.setRotation(0); configuration.windowConfiguration.setBounds( - new Rect(0, 0, isLandscape ? 2160 : 1080, isLandscape ? 1080 : 2160)); + new Rect(0, 0, 1080, 2160)); return configuration; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java index 698315a77d8e..9bb54a18063f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitWindowManagerTests.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.when; import android.content.res.Configuration; import android.graphics.Rect; -import android.view.SurfaceControl; +import android.view.InsetsState; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -40,8 +40,8 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class SplitWindowManagerTests extends ShellTestCase { - @Mock SurfaceControl mSurfaceControl; @Mock SplitLayout mSplitLayout; + @Mock SplitWindowManager.ParentContainerCallbacks mCallbacks; private SplitWindowManager mSplitWindowManager; @Before @@ -50,7 +50,7 @@ public class SplitWindowManagerTests extends ShellTestCase { final Configuration configuration = new Configuration(); configuration.setToDefaults(); mSplitWindowManager = new SplitWindowManager("TestSplitDivider", mContext, configuration, - b -> b.setParent(mSurfaceControl)); + mCallbacks); when(mSplitLayout.getDividerBounds()).thenReturn( new Rect(0, 0, configuration.windowConfiguration.getBounds().width(), configuration.windowConfiguration.getBounds().height())); @@ -59,7 +59,7 @@ public class SplitWindowManagerTests extends ShellTestCase { @Test @UiThreadTest public void testInitRelease() { - mSplitWindowManager.init(mSplitLayout); + mSplitWindowManager.init(mSplitLayout, new InsetsState()); assertThat(mSplitWindowManager.getSurfaceControl()).isNotNull(); mSplitWindowManager.release(); assertThat(mSplitWindowManager.getSurfaceControl()).isNull(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java new file mode 100644 index 000000000000..f622edb7f134 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2021 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.compatui; + +import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.res.Configuration; +import android.testing.AndroidTestingRunner; +import android.view.InsetsSource; +import android.view.InsetsState; + +import androidx.test.filters.SmallTest; + +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.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link CompatUIController}. + * + * Build/Install/Run: + * atest WMShellUnitTests:CompatUIControllerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class CompatUIControllerTest extends ShellTestCase { + private static final int DISPLAY_ID = 0; + private static final int TASK_ID = 12; + + private CompatUIController mController; + private @Mock DisplayController mMockDisplayController; + private @Mock DisplayInsetsController mMockDisplayInsetsController; + private @Mock DisplayLayout mMockDisplayLayout; + private @Mock DisplayImeController mMockImeController; + private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; + private @Mock SyncTransactionQueue mMockSyncQueue; + private @Mock ShellExecutor mMockExecutor; + private @Mock CompatUIWindowManager mMockLayout; + + @Captor + ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + doReturn(mMockDisplayLayout).when(mMockDisplayController).getDisplayLayout(anyInt()); + doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId(); + doReturn(TASK_ID).when(mMockLayout).getTaskId(); + mController = new CompatUIController(mContext, mMockDisplayController, + mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor) { + @Override + CompatUIWindowManager createLayout(Context context, int displayId, int taskId, + Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { + return mMockLayout; + } + }; + spyOn(mController); + } + + @Test + public void testListenerRegistered() { + verify(mMockDisplayController).addDisplayWindowListener(mController); + verify(mMockImeController).addPositionProcessor(mController); + } + + @Test + public void testOnCompatInfoChanged() { + final Configuration taskConfig = new Configuration(); + + // Verify that the restart button is added with non-null size compat info. + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig), + eq(mMockTaskListener)); + + // Verify that the restart button is updated with non-null new size compat info. + final Configuration newTaskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, mMockTaskListener); + + verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener, + true /* show */); + + // Verify that the restart button is removed with null size compat info. + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, null, mMockTaskListener); + + verify(mMockLayout).release(); + } + + @Test + public void testOnDisplayAdded() { + mController.onDisplayAdded(DISPLAY_ID); + mController.onDisplayAdded(DISPLAY_ID + 1); + + verify(mMockDisplayInsetsController).addInsetsChangedListener(eq(DISPLAY_ID), any()); + verify(mMockDisplayInsetsController).addInsetsChangedListener(eq(DISPLAY_ID + 1), any()); + } + + @Test + public void testOnDisplayRemoved() { + mController.onDisplayAdded(DISPLAY_ID); + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockTaskListener); + + mController.onDisplayRemoved(DISPLAY_ID + 1); + + verify(mMockLayout, never()).release(); + verify(mMockDisplayInsetsController, never()).removeInsetsChangedListener(eq(DISPLAY_ID), + any()); + + mController.onDisplayRemoved(DISPLAY_ID); + + verify(mMockDisplayInsetsController).removeInsetsChangedListener(eq(DISPLAY_ID), any()); + verify(mMockLayout).release(); + } + + @Test + public void testOnDisplayConfigurationChanged() { + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockTaskListener); + + final Configuration newTaskConfig = new Configuration(); + mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, newTaskConfig); + + verify(mMockLayout, never()).updateDisplayLayout(any()); + + mController.onDisplayConfigurationChanged(DISPLAY_ID, newTaskConfig); + + verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout); + } + + @Test + public void testInsetsChanged() { + mController.onDisplayAdded(DISPLAY_ID); + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, + mMockTaskListener); + InsetsState insetsState = new InsetsState(); + InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR); + insetsSource.setFrame(0, 0, 1000, 1000); + insetsState.addSource(insetsSource); + + verify(mMockDisplayInsetsController).addInsetsChangedListener(eq(DISPLAY_ID), + mOnInsetsChangedListenerCaptor.capture()); + mOnInsetsChangedListenerCaptor.getValue().insetsChanged(insetsState); + + verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout); + + // No update if the insets state is the same. + clearInvocations(mMockLayout); + mOnInsetsChangedListenerCaptor.getValue().insetsChanged(new InsetsState(insetsState)); + verify(mMockLayout, never()).updateDisplayLayout(mMockDisplayLayout); + } + + @Test + public void testChangeButtonVisibilityOnImeShowHide() { + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + // Verify that the restart button is hidden after IME is showing. + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + + verify(mMockLayout).updateVisibility(false); + + // Verify button remains hidden while IME is showing. + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener, + false /* show */); + + // Verify button is shown after IME is hidden. + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockLayout).updateVisibility(true); + } + + @Test + public void testChangeButtonVisibilityOnKeyguardOccludedChanged() { + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + // Verify that the restart button is hidden after keyguard becomes occluded. + mController.onKeyguardOccludedChanged(true); + + verify(mMockLayout).updateVisibility(false); + + // Verify button remains hidden while keyguard is occluded. + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + verify(mMockLayout).updateCompatInfo(taskConfig, mMockTaskListener, + false /* show */); + + // Verify button is shown after keyguard becomes not occluded. + mController.onKeyguardOccludedChanged(false); + + verify(mMockLayout).updateVisibility(true); + } + + @Test + public void testButtonRemainsHiddenOnKeyguardOccludedFalseWhenImeIsShowing() { + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + mController.onKeyguardOccludedChanged(true); + + verify(mMockLayout, times(2)).updateVisibility(false); + + clearInvocations(mMockLayout); + + // Verify button remains hidden after keyguard becomes not occluded since IME is showing. + mController.onKeyguardOccludedChanged(false); + + verify(mMockLayout).updateVisibility(false); + + // Verify button is shown after IME is not showing. + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockLayout).updateVisibility(true); + } + + @Test + public void testButtonRemainsHiddenOnImeHideWhenKeyguardIsOccluded() { + final Configuration taskConfig = new Configuration(); + mController.onCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, mMockTaskListener); + + mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); + mController.onKeyguardOccludedChanged(true); + + verify(mMockLayout, times(2)).updateVisibility(false); + + clearInvocations(mMockLayout); + + // Verify button remains hidden after IME is hidden since keyguard is occluded. + mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); + + verify(mMockLayout).updateVisibility(false); + + // Verify button is shown after keyguard becomes not occluded. + mController.onKeyguardOccludedChanged(false); + + verify(mMockLayout).updateVisibility(true); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index a20a5e9e8d91..2c3987bc358d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatRestartButtonTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -14,17 +14,20 @@ * limitations under the License. */ -package com.android.wm.shell.sizecompatui; +package com.android.wm.shell.compatui; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import android.content.res.Configuration; import android.testing.AndroidTestingRunner; import android.view.LayoutInflater; +import android.view.SurfaceControlViewHost; import android.widget.ImageButton; +import android.widget.LinearLayout; import androidx.test.filters.SmallTest; @@ -41,55 +44,68 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; /** - * Tests for {@link SizeCompatRestartButton}. + * Tests for {@link CompatUILayout}. * * Build/Install/Run: - * atest WMShellUnitTests:SizeCompatRestartButtonTest + * atest WMShellUnitTests:CompatUILayoutTest */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class SizeCompatRestartButtonTest extends ShellTestCase { +public class CompatUILayoutTest extends ShellTestCase { private static final int TASK_ID = 1; @Mock private SyncTransactionQueue mSyncTransactionQueue; - @Mock private SizeCompatUIController.SizeCompatUICallback mCallback; + @Mock private CompatUIController.CompatUICallback mCallback; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; - @Mock private DisplayLayout mDisplayLayout; + @Mock private SurfaceControlViewHost mViewHost; - private SizeCompatUILayout mLayout; - private SizeCompatRestartButton mButton; + private CompatUIWindowManager mWindowManager; + private CompatUILayout mCompatUILayout; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mCallback, mContext, - new Configuration(), TASK_ID, mTaskListener, mDisplayLayout, + mWindowManager = new CompatUIWindowManager(mContext, new Configuration(), + mSyncTransactionQueue, mCallback, TASK_ID, mTaskListener, new DisplayLayout(), false /* hasShownHint */); - mButton = (SizeCompatRestartButton) - LayoutInflater.from(mContext).inflate(R.layout.size_compat_ui, null); - mButton.inject(mLayout); - spyOn(mLayout); + mCompatUILayout = (CompatUILayout) + LayoutInflater.from(mContext).inflate(R.layout.compat_ui_layout, null); + mCompatUILayout.inject(mWindowManager); + + spyOn(mWindowManager); + spyOn(mCompatUILayout); + doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); } @Test - public void testOnClick() { - final ImageButton button = mButton.findViewById(R.id.size_compat_restart_button); + public void testOnClickForRestartButton() { + final ImageButton button = mCompatUILayout.findViewById(R.id.size_compat_restart_button); button.performClick(); - verify(mLayout).onRestartButtonClicked(); + verify(mWindowManager).onRestartButtonClicked(); + doReturn(mCompatUILayout).when(mWindowManager).inflateCompatUILayout(); verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID); } @Test - public void testOnLongClick() { - doNothing().when(mLayout).onRestartButtonLongClicked(); + public void testOnLongClickForRestartButton() { + doNothing().when(mWindowManager).onRestartButtonLongClicked(); - final ImageButton button = mButton.findViewById(R.id.size_compat_restart_button); + final ImageButton button = mCompatUILayout.findViewById(R.id.size_compat_restart_button); button.performLongClick(); - verify(mLayout).onRestartButtonLongClicked(); + verify(mWindowManager).onRestartButtonLongClicked(); + } + + @Test + public void testOnClickForSizeCompatHint() { + mWindowManager.createLayout(true /* show */); + final LinearLayout sizeCompatHint = mCompatUILayout.findViewById(R.id.size_compat_hint); + sizeCompatHint.performClick(); + + verify(mCompatUILayout).setSizeCompatHintVisibility(/* show= */ false); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java new file mode 100644 index 000000000000..d5dcf2e11a46 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2021 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.compatui; + +import static android.view.InsetsState.ITYPE_EXTRA_NAVIGATION_BAR; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.view.DisplayInfo; +import android.view.InsetsSource; +import android.view.InsetsState; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.SyncTransactionQueue; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for {@link CompatUIWindowManager}. + * + * Build/Install/Run: + * atest WMShellUnitTests:CompatUIWindowManagerTest + */ +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class CompatUIWindowManagerTest extends ShellTestCase { + + private static final int TASK_ID = 1; + + @Mock private SyncTransactionQueue mSyncTransactionQueue; + @Mock private CompatUIController.CompatUICallback mCallback; + @Mock private ShellTaskOrganizer.TaskListener mTaskListener; + @Mock private CompatUILayout mCompatUILayout; + @Mock private SurfaceControlViewHost mViewHost; + private Configuration mTaskConfig; + + private CompatUIWindowManager mWindowManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mTaskConfig = new Configuration(); + + mWindowManager = new CompatUIWindowManager(mContext, new Configuration(), + mSyncTransactionQueue, mCallback, TASK_ID, mTaskListener, new DisplayLayout(), + false /* hasShownHint */); + + spyOn(mWindowManager); + doReturn(mCompatUILayout).when(mWindowManager).inflateCompatUILayout(); + doReturn(mViewHost).when(mWindowManager).createSurfaceViewHost(); + } + + @Test + public void testCreateSizeCompatButton() { + // Not create layout if show is false. + mWindowManager.createLayout(false /* show */); + + verify(mWindowManager, never()).inflateCompatUILayout(); + + // Not create hint popup. + mWindowManager.mShouldShowHint = false; + mWindowManager.createLayout(true /* show */); + + verify(mWindowManager).inflateCompatUILayout(); + verify(mCompatUILayout).setSizeCompatHintVisibility(false /* show */); + + // Create hint popup. + mWindowManager.release(); + mWindowManager.mShouldShowHint = true; + mWindowManager.createLayout(true /* show */); + + verify(mWindowManager, times(2)).inflateCompatUILayout(); + assertNotNull(mCompatUILayout); + verify(mCompatUILayout).setSizeCompatHintVisibility(true /* show */); + assertFalse(mWindowManager.mShouldShowHint); + } + + @Test + public void testRelease() { + mWindowManager.createLayout(true /* show */); + + verify(mWindowManager).inflateCompatUILayout(); + + mWindowManager.release(); + + verify(mViewHost).release(); + } + + @Test + public void testUpdateCompatInfo() { + mWindowManager.createLayout(true /* show */); + + // No diff + clearInvocations(mWindowManager); + mWindowManager.updateCompatInfo(mTaskConfig, mTaskListener, true /* show */); + + verify(mWindowManager, never()).updateSurfacePosition(); + verify(mWindowManager, never()).release(); + verify(mWindowManager, never()).createLayout(anyBoolean()); + + // Change task listener, recreate button. + clearInvocations(mWindowManager); + final ShellTaskOrganizer.TaskListener newTaskListener = mock( + ShellTaskOrganizer.TaskListener.class); + mWindowManager.updateCompatInfo(mTaskConfig, newTaskListener, + true /* show */); + + verify(mWindowManager).release(); + verify(mWindowManager).createLayout(anyBoolean()); + + // Change task bounds, update position. + clearInvocations(mWindowManager); + final Configuration newTaskConfiguration = new Configuration(); + newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); + mWindowManager.updateCompatInfo(newTaskConfiguration, newTaskListener, + true /* show */); + + verify(mWindowManager).updateSurfacePosition(); + } + + @Test + public void testUpdateDisplayLayout() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + + mWindowManager.updateDisplayLayout(displayLayout1); + verify(mWindowManager).updateSurfacePosition(); + + // No update if the display bounds is the same. + clearInvocations(mWindowManager); + final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ false, /* hasStatusBar= */ false); + mWindowManager.updateDisplayLayout(displayLayout2); + verify(mWindowManager, never()).updateSurfacePosition(); + } + + @Test + public void testUpdateDisplayLayoutInsets() { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = 1000; + displayInfo.logicalHeight = 2000; + final DisplayLayout displayLayout = new DisplayLayout(displayInfo, + mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false); + + mWindowManager.updateDisplayLayout(displayLayout); + verify(mWindowManager).updateSurfacePosition(); + + // Update if the insets change on the existing display layout + clearInvocations(mWindowManager); + InsetsState insetsState = new InsetsState(); + InsetsSource insetsSource = new InsetsSource(ITYPE_EXTRA_NAVIGATION_BAR); + insetsSource.setFrame(0, 0, 1000, 1000); + insetsState.addSource(insetsSource); + displayLayout.setInsets(mContext.getResources(), insetsState); + mWindowManager.updateDisplayLayout(displayLayout); + verify(mWindowManager).updateSurfacePosition(); + } + + @Test + public void testUpdateVisibility() { + // Create button if it is not created. + mWindowManager.mCompatUILayout = null; + mWindowManager.updateVisibility(true /* show */); + + verify(mWindowManager).createLayout(true /* show */); + + // Hide button. + clearInvocations(mWindowManager); + doReturn(View.VISIBLE).when(mCompatUILayout).getVisibility(); + mWindowManager.updateVisibility(false /* show */); + + verify(mWindowManager, never()).createLayout(anyBoolean()); + verify(mCompatUILayout).setVisibility(View.GONE); + + // Show button. + doReturn(View.GONE).when(mCompatUILayout).getVisibility(); + mWindowManager.updateVisibility(true /* show */); + + verify(mWindowManager, never()).createLayout(anyBoolean()); + verify(mCompatUILayout).setVisibility(View.VISIBLE); + } + + @Test + public void testAttachToParentSurface() { + final SurfaceControl.Builder b = new SurfaceControl.Builder(); + mWindowManager.attachToParentSurface(b); + + verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b); + } + + @Test + public void testOnRestartButtonClicked() { + mWindowManager.onRestartButtonClicked(); + + verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID); + } + + @Test + public void testOnRestartButtonLongClicked_showHint() { + // Not create hint popup. + mWindowManager.mShouldShowHint = false; + mWindowManager.createLayout(true /* show */); + + verify(mWindowManager).inflateCompatUILayout(); + verify(mCompatUILayout).setSizeCompatHintVisibility(false /* show */); + + mWindowManager.onRestartButtonLongClicked(); + + verify(mCompatUILayout).setSizeCompatHintVisibility(true /* show */); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java new file mode 100644 index 000000000000..9f745208d3ed --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 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.draganddrop; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.os.RemoteException; +import android.view.Display; +import android.view.DragEvent; +import android.view.View; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.internal.logging.UiEventLogger; +import com.android.launcher3.icons.IconProvider; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Tests for the drag and drop controller. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class DragAndDropControllerTest { + + @Mock + private Context mContext; + + @Mock + private DisplayController mDisplayController; + + @Mock + private UiEventLogger mUiEventLogger; + + private DragAndDropController mController; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + mController = new DragAndDropController(mContext, mDisplayController, mUiEventLogger, + mock(IconProvider.class), mock(ShellExecutor.class)); + } + + @Test + public void testIgnoreNonDefaultDisplays() { + final int nonDefaultDisplayId = 12345; + final View dragLayout = mock(View.class); + final Display display = mock(Display.class); + doReturn(nonDefaultDisplayId).when(display).getDisplayId(); + doReturn(display).when(dragLayout).getDisplay(); + + // Expect no per-display layout to be added + mController.onDisplayAdded(nonDefaultDisplayId); + assertFalse(mController.onDrag(dragLayout, mock(DragEvent.class))); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java index ba73d555e334..fe66e225ad4a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java @@ -24,15 +24,14 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY; import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_FULLSCREEN; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT; import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_TOP; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -64,6 +63,7 @@ import android.view.DisplayInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.logging.InstanceId; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.draganddrop.DragAndDropPolicy.Target; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -95,6 +95,9 @@ public class DragAndDropPolicyTest { @Mock private SplitScreenController mSplitScreenStarter; + @Mock + private InstanceId mLoggerSessionId; + private DisplayLayout mLandscapeDisplayLayout; private DisplayLayout mPortraitDisplayLayout; private Insets mInsets; @@ -144,7 +147,6 @@ public class DragAndDropPolicyTest { mSplitPrimaryAppTask = createTaskInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_STANDARD); - setInSplitScreen(false); setRunningTask(mFullscreenAppTask); } @@ -193,122 +195,56 @@ public class DragAndDropPolicyTest { : ActivityInfo.RESIZE_MODE_UNRESIZEABLE; } - private void setInSplitScreen(boolean inSplitscreen) { - doReturn(inSplitscreen).when(mSplitScreenStarter).isSplitScreenVisible(); - } - @Test public void testDragAppOverFullscreenHome_expectOnlyFullscreenTarget() { setRunningTask(mHomeTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( mPolicy.getTargets(mInsets), TYPE_FULLSCREEN); mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(SPLIT_POSITION_UNDEFINED), any()); } @Test - public void testDragAppOverFullscreenApp_expectSplitScreenAndFullscreenTargets() { + public void testDragAppOverFullscreenApp_expectSplitScreenTargets() { setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + mPolicy.getTargets(mInsets), TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_LEFT), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(SPLIT_POSITION_TOP_OR_LEFT), any()); reset(mSplitScreenStarter); mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); } @Test - public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenAndFullscreenTargets() { + public void testDragAppOverFullscreenAppPhone_expectVerticalSplitScreenTargets() { setRunningTask(mFullscreenAppTask); - mPolicy.start(mPortraitDisplayLayout, mActivityClipData); - ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); - reset(mSplitScreenStarter); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); - } - - @Test - public void testDragAppOverFullscreenNonResizeableApp_expectOnlyFullscreenTargets() { - setRunningTask(mNonResizeableFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mPortraitDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); + mPolicy.getTargets(mInsets), TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); - } - - @Test - public void testDragNonResizeableAppOverFullscreenApp_expectOnlyFullscreenTargets() { - setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mNonResizeableActivityClipData); - ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); - } - - @Test - public void testDragAppOverSplitApp_expectFullscreenAndSplitTargets() { - setInSplitScreen(true); - setRunningTask(mSplitPrimaryAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); - ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_LEFT, TYPE_SPLIT_RIGHT); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); - reset(mSplitScreenStarter); - - // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs - mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_RIGHT), mActivityClipData); - verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); - } - - @Test - public void testDragAppOverSplitAppPhone_expectFullscreenAndVerticalSplitTargets() { - setInSplitScreen(true); - setRunningTask(mSplitPrimaryAppTask); - mPolicy.start(mPortraitDisplayLayout, mActivityClipData); - ArrayList<Target> targets = assertExactTargetTypes( - mPolicy.getTargets(mInsets), TYPE_FULLSCREEN, TYPE_SPLIT_TOP, TYPE_SPLIT_BOTTOM); - - mPolicy.handleDrop(filterTargetByType(targets, TYPE_FULLSCREEN), mActivityClipData); + mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_TOP), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_UNDEFINED), eq(SPLIT_POSITION_UNDEFINED), any()); + eq(SPLIT_POSITION_TOP_OR_LEFT), any()); reset(mSplitScreenStarter); - // TODO(b/169894807): Just verify starting for the non-docked task until we have app pairs mPolicy.handleDrop(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), mActivityClipData); verify(mSplitScreenStarter).startIntent(any(), any(), - eq(STAGE_TYPE_SIDE), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()); } @Test public void testTargetHitRects() { setRunningTask(mFullscreenAppTask); - mPolicy.start(mLandscapeDisplayLayout, mActivityClipData); + mPolicy.start(mLandscapeDisplayLayout, mActivityClipData, mLoggerSessionId); ArrayList<Target> targets = mPolicy.getTargets(mInsets); for (Target t : targets) { assertTrue(mPolicy.getTargetAtLocation(t.hitRegion.left, t.hitRegion.top) == t); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java new file mode 100644 index 000000000000..9cbdf1e2dbb6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/fullscreen/FullscreenTaskListenerTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2021 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.fullscreen; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager.RunningTaskInfo; +import android.app.WindowConfiguration; +import android.content.res.Configuration; +import android.graphics.Point; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.recents.RecentTasksController; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@SmallTest +public class FullscreenTaskListenerTest { + + @Mock + private SyncTransactionQueue mSyncQueue; + @Mock + private FullscreenUnfoldController mUnfoldController; + @Mock + private RecentTasksController mRecentTasksController; + @Mock + private SurfaceControl mSurfaceControl; + + private Optional<FullscreenUnfoldController> mFullscreenUnfoldController; + + private FullscreenTaskListener mListener; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mFullscreenUnfoldController = Optional.of(mUnfoldController); + mListener = new FullscreenTaskListener(mSyncQueue, mFullscreenUnfoldController, + Optional.empty()); + } + + @Test + public void testAnimatableTaskAppeared_notifiesUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ true, /* taskId */ 0); + + mListener.onTaskAppeared(info, mSurfaceControl); + + verify(mUnfoldController).onTaskAppeared(eq(info), any()); + } + + @Test + public void testMultipleAnimatableTasksAppeared_notifiesUnfoldController() { + RunningTaskInfo animatable1 = createTaskInfo(/* visible */ true, /* taskId */ 0); + RunningTaskInfo animatable2 = createTaskInfo(/* visible */ true, /* taskId */ 1); + + mListener.onTaskAppeared(animatable1, mSurfaceControl); + mListener.onTaskAppeared(animatable2, mSurfaceControl); + + InOrder order = inOrder(mUnfoldController); + order.verify(mUnfoldController).onTaskAppeared(eq(animatable1), any()); + order.verify(mUnfoldController).onTaskAppeared(eq(animatable2), any()); + } + + @Test + public void testNonAnimatableTaskAppeared_doesNotNotifyUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); + + mListener.onTaskAppeared(info, mSurfaceControl); + + verifyNoMoreInteractions(mUnfoldController); + } + + @Test + public void testNonAnimatableTaskChanged_doesNotNotifyUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); + mListener.onTaskAppeared(info, mSurfaceControl); + + mListener.onTaskInfoChanged(info); + + verifyNoMoreInteractions(mUnfoldController); + } + + @Test + public void testNonAnimatableTaskVanished_doesNotNotifyUnfoldController() { + RunningTaskInfo info = createTaskInfo(/* visible */ false, /* taskId */ 0); + mListener.onTaskAppeared(info, mSurfaceControl); + + mListener.onTaskVanished(info); + + verifyNoMoreInteractions(mUnfoldController); + } + + @Test + public void testAnimatableTaskBecameInactive_notifiesUnfoldController() { + RunningTaskInfo animatableTask = createTaskInfo(/* visible */ true, /* taskId */ 0); + mListener.onTaskAppeared(animatableTask, mSurfaceControl); + RunningTaskInfo notAnimatableTask = createTaskInfo(/* visible */ false, /* taskId */ 0); + + mListener.onTaskInfoChanged(notAnimatableTask); + + verify(mUnfoldController).onTaskVanished(eq(notAnimatableTask)); + } + + @Test + public void testAnimatableTaskVanished_notifiesUnfoldController() { + RunningTaskInfo taskInfo = createTaskInfo(/* visible */ true, /* taskId */ 0); + mListener.onTaskAppeared(taskInfo, mSurfaceControl); + + mListener.onTaskVanished(taskInfo); + + verify(mUnfoldController).onTaskVanished(eq(taskInfo)); + } + + private RunningTaskInfo createTaskInfo(boolean visible, int taskId) { + final RunningTaskInfo info = spy(new RunningTaskInfo()); + info.isVisible = visible; + info.positionInParent = new Point(); + when(info.getWindowingMode()).thenReturn(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + final Configuration configuration = new Configuration(); + configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + when(info.getConfiguration()).thenReturn(configuration); + info.taskId = taskId; + return info; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java index 3c124bafc18a..078e2b6cf574 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutOrganizerTest.java @@ -48,7 +48,6 @@ import android.window.WindowContainerToken; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import com.android.internal.R; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; @@ -124,6 +123,7 @@ public class HideDisplayCutoutOrganizerTest { @Test public void testEnableHideDisplayCutout() { + doReturn(mFakeStatusBarHeightPortrait).when(mOrganizer).getStatusBarHeight(); mOrganizer.enableHideDisplayCutout(); verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT); @@ -154,8 +154,7 @@ public class HideDisplayCutoutOrganizerTest { doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) .getDisplayCutoutInsetsOfNaturalOrientation(); - mContext.getOrCreateTestableResources().addOverride( - R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait); + doReturn(mFakeStatusBarHeightPortrait).when(mOrganizer).getStatusBarHeight(); doReturn(Surface.ROTATION_0).when(mDisplayLayout).rotation(); mOrganizer.enableHideDisplayCutout(); @@ -173,8 +172,7 @@ public class HideDisplayCutoutOrganizerTest { doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) .getDisplayCutoutInsetsOfNaturalOrientation(); - mContext.getOrCreateTestableResources().addOverride( - R.dimen.status_bar_height_landscape, mFakeStatusBarHeightLandscape); + doReturn(mFakeStatusBarHeightLandscape).when(mOrganizer).getStatusBarHeight(); doReturn(Surface.ROTATION_90).when(mDisplayLayout).rotation(); mOrganizer.enableHideDisplayCutout(); @@ -192,8 +190,7 @@ public class HideDisplayCutoutOrganizerTest { doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) .getDisplayCutoutInsetsOfNaturalOrientation(); - mContext.getOrCreateTestableResources().addOverride( - R.dimen.status_bar_height_landscape, mFakeStatusBarHeightLandscape); + doReturn(mFakeStatusBarHeightLandscape).when(mOrganizer).getStatusBarHeight(); doReturn(Surface.ROTATION_270).when(mDisplayLayout).rotation(); mOrganizer.enableHideDisplayCutout(); @@ -211,8 +208,7 @@ public class HideDisplayCutoutOrganizerTest { doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation(); doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) .getDisplayCutoutInsetsOfNaturalOrientation(); - mContext.getOrCreateTestableResources().addOverride( - R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait); + doReturn(mFakeStatusBarHeightPortrait).when(mOrganizer).getStatusBarHeight(); mOrganizer.enableHideDisplayCutout(); // disable hide display cutout @@ -230,8 +226,7 @@ public class HideDisplayCutoutOrganizerTest { doReturn(200).when(mDisplayLayout).height(); doReturn(mFakeDefaultCutoutInsets).when(mOrganizer) .getDisplayCutoutInsetsOfNaturalOrientation(); - mContext.getOrCreateTestableResources().addOverride( - R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait); + doReturn(mFakeStatusBarHeightPortrait).when(mOrganizer).getStatusBarHeight(); doReturn(Surface.ROTATION_0).when(mDisplayLayout).rotation(); mOrganizer.enableHideDisplayCutout(); assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 15, 100, 200)); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java index 911fe0753845..0a3a84923053 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java @@ -42,6 +42,7 @@ import android.util.ArrayMap; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; +import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; @@ -332,6 +333,58 @@ public class OneHandedControllerTest extends OneHandedTestCase { } @Test + public void testOneHandedEnabledRotation90ShouldHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + false); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test + public void testOneHandedDisabledRotation90ShouldNotHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(false); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + false); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test + public void testSwipeToNotificationEnabledRotation90ShouldNotHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + true); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, never()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test + public void testSwipeToNotificationDisabledRotation90ShouldHandleRotate() { + when(mMockSettingsUitl.getSettingsOneHandedModeEnabled(any(), anyInt())).thenReturn(true); + when(mMockSettingsUitl.getSettingsSwipeToNotificationEnabled(any(), anyInt())).thenReturn( + false); + final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); + mSpiedOneHandedController.onRotateDisplay(mDisplay.getDisplayId(), Surface.ROTATION_0, + Surface.ROTATION_90, handlerWCT); + + verify(mMockDisplayAreaOrganizer, atLeastOnce()).onRotateDisplay(eq(mContext), + eq(Surface.ROTATION_90), any(WindowContainerTransaction.class)); + } + + @Test public void testStateActive_shortcutRequestActivate_skipActions() { when(mSpiedTransitionState.getState()).thenReturn(STATE_ACTIVE); when(mSpiedTransitionState.isTransitioning()).thenReturn(false); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java index a6215d3347a8..8e30f65cee78 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java @@ -188,7 +188,7 @@ public class PipBoundsStateTest extends ShellTestCase { final Rect newBounds = new Rect(50, 50, 100, 75); mPipBoundsState.setBounds(currentBounds); - mPipBoundsState.setPipExclusionBoundsChangeCallback(callback); + mPipBoundsState.addPipExclusionBoundsChangeCallback(callback); // Setting the listener immediately calls back with the current bounds. verify(callback).accept(currentBounds); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 9d7c82bb8550..0172cf324eea 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -50,6 +50,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.phone.PhonePipMenuController; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; @@ -75,10 +76,12 @@ public class PipTaskOrganizerTest extends ShellTestCase { @Mock private PipTransitionController mMockPipTransitionController; @Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper; @Mock private PipUiEventLogger mMockPipUiEventLogger; - @Mock private Optional<LegacySplitScreenController> mMockOptionalSplitScreen; + @Mock private Optional<LegacySplitScreenController> mMockOptionalLegacySplitScreen; + @Mock private Optional<SplitScreenController> mMockOptionalSplitScreen; @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; private TestShellExecutor mMainExecutor; private PipBoundsState mPipBoundsState; + private PipTransitionState mPipTransitionState; private PipBoundsAlgorithm mPipBoundsAlgorithm; private ComponentName mComponent1; @@ -90,15 +93,17 @@ public class PipTaskOrganizerTest extends ShellTestCase { mComponent1 = new ComponentName(mContext, "component1"); mComponent2 = new ComponentName(mContext, "component2"); mPipBoundsState = new PipBoundsState(mContext); + mPipTransitionState = new PipTransitionState(); mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState, new PipSnapAlgorithm()); mMainExecutor = new TestShellExecutor(); mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mContext, - mMockSyncTransactionQueue, mPipBoundsState, + mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState, mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController, mMockPipSurfaceTransactionHelper, - mMockPipTransitionController, mMockOptionalSplitScreen, mMockDisplayController, - mMockPipUiEventLogger, mMockShellTaskOrganizer, mMainExecutor)); + mMockPipTransitionController, mMockOptionalLegacySplitScreen, + mMockOptionalSplitScreen, mMockDisplayController, mMockPipUiEventLogger, + mMockShellTaskOrganizer, mMainExecutor)); mMainExecutor.flushAll(); preparePipTaskOrg(); } 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 new file mode 100644 index 000000000000..50f6bd7b4927 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2021 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 static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import static java.lang.Integer.MAX_VALUE; + +import android.app.ActivityManager; +import android.content.Context; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TaskStackListenerImpl; +import com.android.wm.shell.util.GroupedRecentTaskInfo; +import com.android.wm.shell.util.StagedSplitBounds; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; + +/** + * Tests for {@link RecentTasksController}. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class RecentTasksControllerTest extends ShellTestCase { + + @Mock + private Context mContext; + @Mock + private TaskStackListenerImpl mTaskStackListener; + + private ShellTaskOrganizer mShellTaskOrganizer; + private RecentTasksController mRecentTasksController; + private ShellExecutor mMainExecutor; + + @Before + public void setUp() { + mMainExecutor = new TestShellExecutor(); + mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener, + mMainExecutor)); + mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext, + null /* sizeCompatUI */, Optional.of(mRecentTasksController)); + } + + @Test + public void testAddRemoveSplitNotifyChange() { + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + setRawList(t1, t2); + + mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, mock(StagedSplitBounds.class)); + verify(mRecentTasksController).notifyRecentTasksChanged(); + + reset(mRecentTasksController); + mRecentTasksController.removeSplitPair(t1.taskId); + verify(mRecentTasksController).notifyRecentTasksChanged(); + } + + @Test + public void testAddSameSplitBoundsInfoSkipNotifyChange() { + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + setRawList(t1, t2); + + // Verify only one update if the split info is the same + StagedSplitBounds bounds1 = new StagedSplitBounds(new Rect(0, 0, 50, 50), + new Rect(50, 50, 100, 100), t1.taskId, t2.taskId); + mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds1); + StagedSplitBounds bounds2 = new StagedSplitBounds(new Rect(0, 0, 50, 50), + new Rect(50, 50, 100, 100), t1.taskId, t2.taskId); + mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, bounds2); + verify(mRecentTasksController, times(1)).notifyRecentTasksChanged(); + } + + @Test + public void testGetRecentTasks() { + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + setRawList(t1, t2, t3); + + ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( + MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); + assertGroupedTasksListEquals(recentTasks, + t1.taskId, -1, + t2.taskId, -1, + t3.taskId, -1); + } + + @Test + public void testGetRecentTasks_withPairs() { + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4); + ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5); + ActivityManager.RecentTaskInfo t6 = makeTaskInfo(6); + setRawList(t1, t2, t3, t4, t5, t6); + + // Mark a couple pairs [t2, t4], [t3, t5] + StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 4); + StagedSplitBounds pair2Bounds = new StagedSplitBounds(new Rect(), new Rect(), 3, 5); + + mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); + mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); + + ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks( + MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0); + assertGroupedTasksListEquals(recentTasks, + t1.taskId, -1, + t2.taskId, t4.taskId, + t3.taskId, t5.taskId, + t6.taskId, -1); + } + + @Test + public void testRemovedTaskRemovesSplit() { + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + setRawList(t1, t2, t3); + + // Add a pair + StagedSplitBounds pair1Bounds = new StagedSplitBounds(new Rect(), new Rect(), 2, 3); + mRecentTasksController.addSplitPair(t2.taskId, t3.taskId, pair1Bounds); + reset(mRecentTasksController); + + // Remove one of the tasks and ensure the pair is removed + SurfaceControl mockLeash = mock(SurfaceControl.class); + ActivityManager.RunningTaskInfo rt2 = makeRunningTaskInfo(2); + mShellTaskOrganizer.onTaskAppeared(rt2, mockLeash); + mShellTaskOrganizer.onTaskVanished(rt2); + + verify(mRecentTasksController).removeSplitPair(t2.taskId); + } + + @Test + public void testTaskWindowingModeChangedNotifiesChange() { + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + setRawList(t1); + + // Remove one of the tasks and ensure the pair is removed + SurfaceControl mockLeash = mock(SurfaceControl.class); + ActivityManager.RunningTaskInfo rt2Fullscreen = makeRunningTaskInfo(2); + rt2Fullscreen.configuration.windowConfiguration.setWindowingMode( + WINDOWING_MODE_FULLSCREEN); + mShellTaskOrganizer.onTaskAppeared(rt2Fullscreen, mockLeash); + + // Change the windowing mode and ensure the recent tasks change is notified + ActivityManager.RunningTaskInfo rt2MultiWIndow = makeRunningTaskInfo(2); + rt2MultiWIndow.configuration.windowConfiguration.setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW); + mShellTaskOrganizer.onTaskInfoChanged(rt2MultiWIndow); + + verify(mRecentTasksController).notifyRecentTasksChanged(); + } + + /** + * Helper to create a task with a given task id. + */ + private ActivityManager.RecentTaskInfo makeTaskInfo(int taskId) { + ActivityManager.RecentTaskInfo info = new ActivityManager.RecentTaskInfo(); + info.taskId = taskId; + return info; + } + + /** + * Helper to create a running task with a given task id. + */ + private ActivityManager.RunningTaskInfo makeRunningTaskInfo(int taskId) { + ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); + info.taskId = taskId; + return info; + } + + /** + * Helper to set the raw task list on the controller. + */ + private ArrayList<ActivityManager.RecentTaskInfo> setRawList( + ActivityManager.RecentTaskInfo... tasks) { + ArrayList<ActivityManager.RecentTaskInfo> rawList = new ArrayList<>(); + for (ActivityManager.RecentTaskInfo task : tasks) { + rawList.add(task); + } + doReturn(rawList).when(mRecentTasksController).getRawRecentTasks(anyInt(), anyInt(), + anyInt()); + return rawList; + } + + /** + * Asserts that the recent tasks matches the given task ids. + * @param expectedTaskIds list of task ids that map to the flattened task ids of the tasks in + * the grouped task list + */ + private void assertGroupedTasksListEquals(ArrayList<GroupedRecentTaskInfo> recentTasks, + int... expectedTaskIds) { + int[] flattenedTaskIds = new int[recentTasks.size() * 2]; + for (int i = 0; i < recentTasks.size(); i++) { + GroupedRecentTaskInfo pair = recentTasks.get(i); + int taskId1 = pair.mTaskInfo1.taskId; + flattenedTaskIds[2 * i] = taskId1; + flattenedTaskIds[2 * i + 1] = pair.mTaskInfo2 != null + ? pair.mTaskInfo2.taskId + : -1; + + if (pair.mTaskInfo2 != null) { + assertNotNull(pair.mStagedSplitBounds); + int leftTopTaskId = pair.mStagedSplitBounds.leftTopTaskId; + int bottomRightTaskId = pair.mStagedSplitBounds.rightBottomTaskId; + // Unclear if pairs are ordered by split position, most likely not. + assertTrue(leftTopTaskId == taskId1 || leftTopTaskId == pair.mTaskInfo2.taskId); + assertTrue(bottomRightTaskId == taskId1 + || bottomRightTaskId == pair.mTaskInfo2.taskId); + } else { + assertNull(pair.mStagedSplitBounds); + } + } + assertTrue("Expected: " + Arrays.toString(expectedTaskIds) + + " Received: " + Arrays.toString(flattenedTaskIds), + Arrays.equals(flattenedTaskIds, expectedTaskIds)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java new file mode 100644 index 000000000000..ad73c56950bd --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/StagedSplitBoundsTest.java @@ -0,0 +1,94 @@ +package com.android.wm.shell.recents; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.wm.shell.util.StagedSplitBounds; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class StagedSplitBoundsTest { + private static final int DEVICE_WIDTH = 100; + private static final int DEVICE_LENGTH = 200; + private static final int DIVIDER_SIZE = 20; + private static final int TASK_ID_1 = 4; + private static final int TASK_ID_2 = 9; + + // Bounds in screen space + private final Rect mTopRect = new Rect(); + private final Rect mBottomRect = new Rect(); + private final Rect mLeftRect = new Rect(); + private final Rect mRightRect = new Rect(); + + @Before + public void setup() { + mTopRect.set(0, 0, DEVICE_WIDTH, DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2); + mBottomRect.set(0, DEVICE_LENGTH / 2 + DIVIDER_SIZE / 2, + DEVICE_WIDTH, DEVICE_LENGTH); + mLeftRect.set(0, 0, DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, DEVICE_LENGTH); + mRightRect.set(DEVICE_WIDTH / 2 + DIVIDER_SIZE / 2, 0, + DEVICE_WIDTH, DEVICE_LENGTH); + } + + @Test + public void testVerticalStacked() { + StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + TASK_ID_1, TASK_ID_2); + assertTrue(ssb.appsStackedVertically); + } + + @Test + public void testHorizontalStacked() { + StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + TASK_ID_1, TASK_ID_2); + assertFalse(ssb.appsStackedVertically); + } + + @Test + public void testHorizontalDividerBounds() { + StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + TASK_ID_1, TASK_ID_2); + Rect dividerBounds = ssb.visualDividerBounds; + assertEquals(0, dividerBounds.left); + assertEquals(DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2, dividerBounds.top); + assertEquals(DEVICE_WIDTH, dividerBounds.right); + assertEquals(DEVICE_LENGTH / 2 + DIVIDER_SIZE / 2, dividerBounds.bottom); + } + + @Test + public void testVerticalDividerBounds() { + StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + TASK_ID_1, TASK_ID_2); + Rect dividerBounds = ssb.visualDividerBounds; + assertEquals(DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2, dividerBounds.left); + assertEquals(0, dividerBounds.top); + assertEquals(DEVICE_WIDTH / 2 + DIVIDER_SIZE / 2, dividerBounds.right); + assertEquals(DEVICE_LENGTH, dividerBounds.bottom); + } + + @Test + public void testEqualVerticalTaskPercent() { + StagedSplitBounds ssb = new StagedSplitBounds(mTopRect, mBottomRect, + TASK_ID_1, TASK_ID_2); + float topPercentSpaceTaken = (float) (DEVICE_LENGTH / 2 - DIVIDER_SIZE / 2) / DEVICE_LENGTH; + assertEquals(topPercentSpaceTaken, ssb.topTaskPercent, 0.01); + } + + @Test + public void testEqualHorizontalTaskPercent() { + StagedSplitBounds ssb = new StagedSplitBounds(mLeftRect, mRightRect, + TASK_ID_1, TASK_ID_2); + float leftPercentSpaceTaken = (float) (DEVICE_WIDTH / 2 - DIVIDER_SIZE / 2) / DEVICE_WIDTH; + assertEquals(leftPercentSpaceTaken, ssb.leftTaskPercent, 0.01); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java deleted file mode 100644 index 10fd7d705967..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatHintPopupTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; - -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.verify; - -import android.content.res.Configuration; -import android.testing.AndroidTestingRunner; -import android.view.LayoutInflater; -import android.widget.Button; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.R; -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.SyncTransactionQueue; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Tests for {@link SizeCompatHintPopup}. - * - * Build/Install/Run: - * atest WMShellUnitTests:SizeCompatHintPopupTest - */ -@RunWith(AndroidTestingRunner.class) -@SmallTest -public class SizeCompatHintPopupTest extends ShellTestCase { - - @Mock private SyncTransactionQueue mSyncTransactionQueue; - @Mock private SizeCompatUIController.SizeCompatUICallback mCallback; - @Mock private ShellTaskOrganizer.TaskListener mTaskListener; - @Mock private DisplayLayout mDisplayLayout; - - private SizeCompatUILayout mLayout; - private SizeCompatHintPopup mHint; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - final int taskId = 1; - mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mCallback, mContext, - new Configuration(), taskId, mTaskListener, mDisplayLayout, - false /* hasShownHint */); - mHint = (SizeCompatHintPopup) - LayoutInflater.from(mContext).inflate(R.layout.size_compat_mode_hint, null); - mHint.inject(mLayout); - - spyOn(mLayout); - } - - @Test - public void testOnClick() { - doNothing().when(mLayout).dismissHint(); - - final Button button = mHint.findViewById(R.id.got_it); - button.performClick(); - - verify(mLayout).dismissHint(); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java deleted file mode 100644 index 8839f58ea889..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUIControllerTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.content.res.Configuration; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -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.DisplayImeController; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.SyncTransactionQueue; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Tests for {@link SizeCompatUIController}. - * - * Build/Install/Run: - * atest WMShellUnitTests:SizeCompatUIControllerTest - */ -@RunWith(AndroidTestingRunner.class) -@SmallTest -public class SizeCompatUIControllerTest extends ShellTestCase { - private static final int DISPLAY_ID = 0; - private static final int TASK_ID = 12; - - private SizeCompatUIController mController; - private @Mock DisplayController mMockDisplayController; - private @Mock DisplayLayout mMockDisplayLayout; - private @Mock DisplayImeController mMockImeController; - private @Mock ShellTaskOrganizer.TaskListener mMockTaskListener; - private @Mock SyncTransactionQueue mMockSyncQueue; - private @Mock SizeCompatUILayout mMockLayout; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - doReturn(mMockDisplayLayout).when(mMockDisplayController).getDisplayLayout(anyInt()); - doReturn(DISPLAY_ID).when(mMockLayout).getDisplayId(); - doReturn(TASK_ID).when(mMockLayout).getTaskId(); - mController = new SizeCompatUIController(mContext, mMockDisplayController, - mMockImeController, mMockSyncQueue) { - @Override - SizeCompatUILayout createLayout(Context context, int displayId, int taskId, - Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { - return mMockLayout; - } - }; - spyOn(mController); - } - - @Test - public void testListenerRegistered() { - verify(mMockDisplayController).addDisplayWindowListener(mController); - verify(mMockImeController).addPositionProcessor(mController); - } - - @Test - public void testOnSizeCompatInfoChanged() { - final Configuration taskConfig = new Configuration(); - - // Verify that the restart button is added with non-null size compat info. - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); - - verify(mController).createLayout(any(), eq(DISPLAY_ID), eq(TASK_ID), eq(taskConfig), - eq(mMockTaskListener)); - - // Verify that the restart button is updated with non-null new size compat info. - final Configuration newTaskConfig = new Configuration(); - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, newTaskConfig, - mMockTaskListener); - - verify(mMockLayout).updateSizeCompatInfo(taskConfig, mMockTaskListener, - false /* isImeShowing */); - - // Verify that the restart button is removed with null size compat info. - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, null, mMockTaskListener); - - verify(mMockLayout).release(); - } - - @Test - public void testOnDisplayRemoved() { - final Configuration taskConfig = new Configuration(); - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); - - mController.onDisplayRemoved(DISPLAY_ID + 1); - - verify(mMockLayout, never()).release(); - - mController.onDisplayRemoved(DISPLAY_ID); - - verify(mMockLayout).release(); - } - - @Test - public void testOnDisplayConfigurationChanged() { - final Configuration taskConfig = new Configuration(); - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); - - final Configuration newTaskConfig = new Configuration(); - mController.onDisplayConfigurationChanged(DISPLAY_ID + 1, newTaskConfig); - - verify(mMockLayout, never()).updateDisplayLayout(any()); - - mController.onDisplayConfigurationChanged(DISPLAY_ID, newTaskConfig); - - verify(mMockLayout).updateDisplayLayout(mMockDisplayLayout); - } - - @Test - public void testChangeButtonVisibilityOnImeShowHide() { - final Configuration taskConfig = new Configuration(); - mController.onSizeCompatInfoChanged(DISPLAY_ID, TASK_ID, taskConfig, - mMockTaskListener); - - mController.onImeVisibilityChanged(DISPLAY_ID, true /* isShowing */); - - verify(mMockLayout).updateImeVisibility(true); - - mController.onImeVisibilityChanged(DISPLAY_ID, false /* isShowing */); - - verify(mMockLayout).updateImeVisibility(false); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java deleted file mode 100644 index ee4c81547bbd..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sizecompatui/SizeCompatUILayoutTest.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 2021 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.sizecompatui; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import android.content.res.Configuration; -import android.graphics.Rect; -import android.testing.AndroidTestingRunner; -import android.view.DisplayInfo; -import android.view.SurfaceControl; -import android.view.View; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.SyncTransactionQueue; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Tests for {@link SizeCompatUILayout}. - * - * Build/Install/Run: - * atest WMShellUnitTests:SizeCompatUILayoutTest - */ -@RunWith(AndroidTestingRunner.class) -@SmallTest -public class SizeCompatUILayoutTest extends ShellTestCase { - - private static final int TASK_ID = 1; - - @Mock private SyncTransactionQueue mSyncTransactionQueue; - @Mock private SizeCompatUIController.SizeCompatUICallback mCallback; - @Mock private ShellTaskOrganizer.TaskListener mTaskListener; - @Mock private DisplayLayout mDisplayLayout; - @Mock private SizeCompatRestartButton mButton; - @Mock private SizeCompatHintPopup mHint; - private Configuration mTaskConfig; - - private SizeCompatUILayout mLayout; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mTaskConfig = new Configuration(); - - mLayout = new SizeCompatUILayout(mSyncTransactionQueue, mCallback, mContext, - new Configuration(), TASK_ID, mTaskListener, mDisplayLayout, - false /* hasShownHint */); - - spyOn(mLayout); - spyOn(mLayout.mButtonWindowManager); - doReturn(mButton).when(mLayout.mButtonWindowManager).createSizeCompatButton(); - - final SizeCompatUIWindowManager hintWindowManager = mLayout.createHintWindowManager(); - spyOn(hintWindowManager); - doReturn(mHint).when(hintWindowManager).createSizeCompatHint(); - doReturn(hintWindowManager).when(mLayout).createHintWindowManager(); - } - - @Test - public void testCreateSizeCompatButton() { - // Not create button if IME is showing. - mLayout.createSizeCompatButton(true /* isImeShowing */); - - verify(mLayout.mButtonWindowManager, never()).createSizeCompatButton(); - assertNull(mLayout.mButton); - assertNull(mLayout.mHintWindowManager); - assertNull(mLayout.mHint); - - // Not create hint popup. - mLayout.mShouldShowHint = false; - mLayout.createSizeCompatButton(false /* isImeShowing */); - - verify(mLayout.mButtonWindowManager).createSizeCompatButton(); - assertNotNull(mLayout.mButton); - assertNull(mLayout.mHintWindowManager); - assertNull(mLayout.mHint); - - // Create hint popup. - mLayout.release(); - mLayout.mShouldShowHint = true; - mLayout.createSizeCompatButton(false /* isImeShowing */); - - verify(mLayout.mButtonWindowManager, times(2)).createSizeCompatButton(); - assertNotNull(mLayout.mButton); - assertNotNull(mLayout.mHintWindowManager); - verify(mLayout.mHintWindowManager).createSizeCompatHint(); - assertNotNull(mLayout.mHint); - assertFalse(mLayout.mShouldShowHint); - } - - @Test - public void testRelease() { - mLayout.createSizeCompatButton(false /* isImeShowing */); - final SizeCompatUIWindowManager hintWindowManager = mLayout.mHintWindowManager; - - mLayout.release(); - - assertNull(mLayout.mButton); - assertNull(mLayout.mHint); - verify(hintWindowManager).release(); - assertNull(mLayout.mHintWindowManager); - verify(mLayout.mButtonWindowManager).release(); - } - - @Test - public void testUpdateSizeCompatInfo() { - mLayout.createSizeCompatButton(false /* isImeShowing */); - - // No diff - clearInvocations(mLayout); - mLayout.updateSizeCompatInfo(mTaskConfig, mTaskListener, - false /* isImeShowing */); - - verify(mLayout, never()).updateButtonSurfacePosition(); - verify(mLayout, never()).release(); - verify(mLayout, never()).createSizeCompatButton(anyBoolean()); - - // Change task listener, recreate button. - clearInvocations(mLayout); - final ShellTaskOrganizer.TaskListener newTaskListener = mock( - ShellTaskOrganizer.TaskListener.class); - mLayout.updateSizeCompatInfo(mTaskConfig, newTaskListener, - false /* isImeShowing */); - - verify(mLayout).release(); - verify(mLayout).createSizeCompatButton(anyBoolean()); - - // Change task bounds, update position. - clearInvocations(mLayout); - final Configuration newTaskConfiguration = new Configuration(); - newTaskConfiguration.windowConfiguration.setBounds(new Rect(0, 1000, 0, 2000)); - mLayout.updateSizeCompatInfo(newTaskConfiguration, newTaskListener, - false /* isImeShowing */); - - verify(mLayout).updateButtonSurfacePosition(); - verify(mLayout).updateHintSurfacePosition(); - } - - @Test - public void testUpdateDisplayLayout() { - final DisplayInfo displayInfo = new DisplayInfo(); - displayInfo.logicalWidth = 1000; - displayInfo.logicalHeight = 2000; - final DisplayLayout displayLayout1 = new DisplayLayout(displayInfo, - mContext.getResources(), false, false); - - mLayout.updateDisplayLayout(displayLayout1); - verify(mLayout).updateButtonSurfacePosition(); - verify(mLayout).updateHintSurfacePosition(); - - // No update if the display bounds is the same. - clearInvocations(mLayout); - final DisplayLayout displayLayout2 = new DisplayLayout(displayInfo, - mContext.getResources(), false, false); - mLayout.updateDisplayLayout(displayLayout2); - verify(mLayout, never()).updateButtonSurfacePosition(); - verify(mLayout, never()).updateHintSurfacePosition(); - } - - @Test - public void testUpdateImeVisibility() { - // Create button if it is not created. - mLayout.mButton = null; - mLayout.updateImeVisibility(false /* isImeShowing */); - - verify(mLayout).createSizeCompatButton(false /* isImeShowing */); - - // Hide button if ime is shown. - clearInvocations(mLayout); - doReturn(View.VISIBLE).when(mButton).getVisibility(); - mLayout.updateImeVisibility(true /* isImeShowing */); - - verify(mLayout, never()).createSizeCompatButton(anyBoolean()); - verify(mButton).setVisibility(View.GONE); - - // Show button if ime is not shown. - doReturn(View.GONE).when(mButton).getVisibility(); - mLayout.updateImeVisibility(false /* isImeShowing */); - - verify(mLayout, never()).createSizeCompatButton(anyBoolean()); - verify(mButton).setVisibility(View.VISIBLE); - } - - @Test - public void testAttachToParentSurface() { - final SurfaceControl.Builder b = new SurfaceControl.Builder(); - mLayout.attachToParentSurface(b); - - verify(mTaskListener).attachChildSurfaceToTask(TASK_ID, b); - } - - @Test - public void testOnRestartButtonClicked() { - mLayout.onRestartButtonClicked(); - - verify(mCallback).onSizeCompatRestartButtonClicked(TASK_ID); - } - - @Test - public void testOnRestartButtonLongClicked_showHint() { - mLayout.dismissHint(); - - assertNull(mLayout.mHint); - - mLayout.onRestartButtonLongClicked(); - - assertNotNull(mLayout.mHint); - } - - @Test - public void testDismissHint() { - mLayout.onRestartButtonLongClicked(); - final SizeCompatUIWindowManager hintWindowManager = mLayout.mHintWindowManager; - assertNotNull(mLayout.mHint); - assertNotNull(hintWindowManager); - - mLayout.dismissHint(); - - assertNull(mLayout.mHint); - assertNull(mLayout.mHintWindowManager); - verify(hintWindowManager).release(); - } -} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java index 1bb5fd1e49e7..c9720671f49c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/MainStageTests.java @@ -25,10 +25,13 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.SyncTransactionQueue; @@ -41,28 +44,31 @@ import org.mockito.MockitoAnnotations; /** Tests for {@link MainStage} */ @SmallTest @RunWith(AndroidJUnit4.class) -public class MainStageTests { +public class MainStageTests extends ShellTestCase { @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; @Mock private SyncTransactionQueue mSyncQueue; @Mock private ActivityManager.RunningTaskInfo mRootTaskInfo; @Mock private SurfaceControl mRootLeash; + @Mock private IconProvider mIconProvider; private WindowContainerTransaction mWct = new WindowContainerTransaction(); private SurfaceSession mSurfaceSession = new SurfaceSession(); private MainStage mMainStage; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); - mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, + mSyncQueue, mSurfaceSession, mIconProvider, null); mMainStage.onTaskAppeared(mRootTaskInfo, mRootLeash); } @Test public void testActiveDeactivate() { - mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct); + mMainStage.activate(mRootTaskInfo.configuration.windowConfiguration.getBounds(), mWct, + true /* reparent */); assertThat(mMainStage.isActive()).isTrue(); mMainStage.deactivate(mWct); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java index 56a005642ce2..a31aa58bdc26 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SideStageTests.java @@ -29,10 +29,13 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.SyncTransactionQueue; @@ -46,22 +49,24 @@ import org.mockito.Spy; /** Tests for {@link SideStage} */ @SmallTest @RunWith(AndroidJUnit4.class) -public class SideStageTests { +public class SideStageTests extends ShellTestCase { @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; @Mock private SyncTransactionQueue mSyncQueue; @Mock private ActivityManager.RunningTaskInfo mRootTask; @Mock private SurfaceControl mRootLeash; + @Mock private IconProvider mIconProvider; @Spy private WindowContainerTransaction mWct; private SurfaceSession mSurfaceSession = new SurfaceSession(); private SideStage mSideStage; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); mRootTask = new TestRunningTaskInfoBuilder().build(); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, + mSyncQueue, mSurfaceSession, mIconProvider, null); mSideStage.onTaskAppeared(mRootTask, mRootLeash); } @@ -69,7 +74,7 @@ public class SideStageTests { public void testAddTask() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - mSideStage.addTask(task, mRootTask.configuration.windowConfiguration.getBounds(), mWct); + mSideStage.addTask(task, mWct); verify(mWct).reparent(eq(task.token), eq(mRootTask.token), eq(true)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index ab6f76996398..aab1e3a99c98 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -18,7 +18,6 @@ package com.android.wm.shell.splitscreen; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER; - import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -33,11 +32,17 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.transition.Transitions; +import java.util.Optional; + +import javax.inject.Provider; + public class SplitTestUtils { static SplitLayout createMockSplitLayout() { @@ -65,9 +70,14 @@ public class SplitTestUtils { TestStageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, RootTaskDisplayAreaOrganizer rootTDAOrganizer, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayImeController imeController, - SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool) { + DisplayInsetsController insetsController, SplitLayout splitLayout, + Transitions transitions, TransactionPool transactionPool, + SplitscreenEventLogger logger, + Optional<RecentTasksController> recentTasks, + Provider<Optional<StageTaskUnfoldController>> unfoldController) { super(context, displayId, syncQueue, rootTDAOrganizer, taskOrganizer, mainStage, - sideStage, imeController, splitLayout, transitions, transactionPool); + sideStage, imeController, insetsController, splitLayout, transitions, + transactionPool, logger, recentTasks, unfoldController); // Prepare default TaskDisplayArea for testing. mDisplayAreaInfo = new DisplayAreaInfo( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index aca80f3556b9..1eae625233a0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -46,22 +46,27 @@ import android.view.SurfaceControl; import android.view.SurfaceSession; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.common.split.SplitLayout; +import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -71,6 +76,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; +import java.util.Optional; + /** Tests for {@link StageCoordinator} */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -79,9 +86,12 @@ public class SplitTransitionTests extends ShellTestCase { @Mock private SyncTransactionQueue mSyncQueue; @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; @Mock private DisplayImeController mDisplayImeController; + @Mock private DisplayInsetsController mDisplayInsetsController; @Mock private TransactionPool mTransactionPool; @Mock private Transitions mTransitions; @Mock private SurfaceSession mSurfaceSession; + @Mock private SplitscreenEventLogger mLogger; + @Mock private IconProvider mIconProvider; private SplitLayout mSplitLayout; private MainStage mMainStage; private SideStage mSideStage; @@ -92,6 +102,7 @@ public class SplitTransitionTests extends ShellTestCase { private ActivityManager.RunningTaskInfo mSideChild; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); final ShellExecutor mockExecutor = mock(ShellExecutor.class); @@ -99,15 +110,18 @@ public class SplitTransitionTests extends ShellTestCase { doReturn(mockExecutor).when(mTransitions).getAnimExecutor(); doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire(); mSplitLayout = SplitTestUtils.createMockSplitLayout(); - mMainStage = new MainStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); + mMainStage = new MainStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + mIconProvider, null); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); - mSideStage = new SideStage(mTaskOrganizer, DEFAULT_DISPLAY, mock( - StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession); + mSideStage = new SideStage(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( + StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession, + mIconProvider, null); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, - mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, - mDisplayImeController, mSplitLayout, mTransitions, mTransactionPool); + mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, + mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, + mTransactionPool, mLogger, Optional.empty(), Optional::empty); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) .when(mTransitions).startTransition(anyInt(), any(), any()); @@ -125,12 +139,13 @@ public class SplitTransitionTests extends ShellTestCase { TestRemoteTransition testRemote = new TestRemoteTransition(); IBinder transition = mSplitScreenTransitions.startEnterTransition( - TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), testRemote, - mStageCoordinator); + TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), + new RemoteTransition(testRemote), mStageCoordinator); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertTrue(accepted); @@ -168,6 +183,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskAppeared(newTask, createMockSurface()); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertFalse(accepted); assertTrue(mStageCoordinator.isSplitScreenVisible()); @@ -188,6 +204,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(newTask); accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertFalse(accepted); assertTrue(mStageCoordinator.isSplitScreenVisible()); @@ -223,6 +240,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(mSideChild); mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertFalse(mStageCoordinator.isSplitScreenVisible()); } @@ -244,6 +262,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(mSideChild); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertTrue(accepted); assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -274,6 +293,7 @@ public class SplitTransitionTests extends ShellTestCase { mSideStage.onTaskVanished(mSideChild); boolean accepted = mStageCoordinator.startAnimation(transition, info, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); assertTrue(accepted); assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -293,13 +313,15 @@ public class SplitTransitionTests extends ShellTestCase { TransitionInfo enterInfo = createEnterPairInfo(); IBinder enterTransit = mSplitScreenTransitions.startEnterTransition( TRANSIT_SPLIT_SCREEN_PAIR_OPEN, new WindowContainerTransaction(), - new TestRemoteTransition(), mStageCoordinator); + new RemoteTransition(new TestRemoteTransition()), mStageCoordinator); mMainStage.onTaskAppeared(mMainChild, createMockSurface()); mSideStage.onTaskAppeared(mSideChild, createMockSurface()); mStageCoordinator.startAnimation(enterTransit, enterInfo, mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mock(Transitions.TransitionFinishCallback.class)); - mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction()); + mMainStage.activate(new Rect(0, 0, 100, 100), new WindowContainerTransaction(), + true /* includingTopTask */); } private boolean containsSplitExit(@NonNull WindowContainerTransaction wct) { @@ -335,10 +357,11 @@ public class SplitTransitionTests extends ShellTestCase { @Override public void startAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) + SurfaceControl.Transaction startTransaction, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { mCalled = true; - finishCallback.onTransitionFinished(mRemoteFinishWCT); + finishCallback.onTransitionFinished(mRemoteFinishWCT, null /* sct */); } @Override 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 06b08686bf4c..85f6789c3435 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 @@ -16,17 +16,32 @@ package com.android.wm.shell.splitscreen; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.wm.shell.common.split.SplitLayout.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.graphics.Rect; +import android.window.DisplayAreaInfo; import android.window.WindowContainerTransaction; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -37,8 +52,10 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.DisplayImeController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.transition.Transitions; import org.junit.Before; @@ -47,37 +64,124 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -/** Tests for {@link StageCoordinator} */ +import java.util.Optional; + +import javax.inject.Provider; + +/** + * Tests for {@link StageCoordinator} + */ @SmallTest @RunWith(AndroidJUnit4.class) public class StageCoordinatorTests extends ShellTestCase { - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; - @Mock private MainStage mMainStage; - @Mock private SideStage mSideStage; - @Mock private DisplayImeController mDisplayImeController; - @Mock private Transitions mTransitions; - @Mock private TransactionPool mTransactionPool; + @Mock + private ShellTaskOrganizer mTaskOrganizer; + @Mock + private SyncTransactionQueue mSyncQueue; + @Mock + private RootTaskDisplayAreaOrganizer mRootTDAOrganizer; + @Mock + private MainStage mMainStage; + @Mock + private SideStage mSideStage; + @Mock + private StageTaskUnfoldController mMainUnfoldController; + @Mock + private StageTaskUnfoldController mSideUnfoldController; + @Mock + private SplitLayout mSplitLayout; + @Mock + private DisplayImeController mDisplayImeController; + @Mock + private DisplayInsetsController mDisplayInsetsController; + @Mock + private Transitions mTransitions; + @Mock + private TransactionPool mTransactionPool; + @Mock + private SplitscreenEventLogger mLogger; + + private final Rect mBounds1 = new Rect(10, 20, 30, 40); + private final Rect mBounds2 = new Rect(5, 10, 15, 20); + private StageCoordinator mStageCoordinator; @Before public void setup() { MockitoAnnotations.initMocks(this); - mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, - mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, - mDisplayImeController, null /* splitLayout */, mTransitions, mTransactionPool); + mStageCoordinator = spy(createStageCoordinator(/* splitLayout */ null)); + doNothing().when(mStageCoordinator).updateActivityOptions(any(), anyInt()); + + when(mSplitLayout.getBounds1()).thenReturn(mBounds1); + when(mSplitLayout.getBounds2()).thenReturn(mBounds2); } @Test - public void testMoveToSideStage() { + public void testMoveToStage() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - mStageCoordinator.moveToSideStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT); + mStageCoordinator.moveToStage(task, STAGE_TYPE_MAIN, SPLIT_POSITION_BOTTOM_OR_RIGHT, + new WindowContainerTransaction()); + verify(mMainStage).addTask(eq(task), any(WindowContainerTransaction.class)); + assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); - verify(mMainStage).activate(any(Rect.class), any(WindowContainerTransaction.class)); - verify(mSideStage).addTask(eq(task), any(Rect.class), - any(WindowContainerTransaction.class)); + mStageCoordinator.moveToStage(task, STAGE_TYPE_SIDE, SPLIT_POSITION_BOTTOM_OR_RIGHT, + new WindowContainerTransaction()); + verify(mSideStage).addTask(eq(task), any(WindowContainerTransaction.class)); + assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); + } + + @Test + public void testMoveToUndefinedStage() { + final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); + + // Verify move to undefined stage while split screen not activated moves task to side stage. + when(mMainStage.isActive()).thenReturn(false); + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); + mStageCoordinator.moveToStage(task, STAGE_TYPE_UNDEFINED, SPLIT_POSITION_BOTTOM_OR_RIGHT, + new WindowContainerTransaction()); + verify(mSideStage).addTask(eq(task), any(WindowContainerTransaction.class)); + assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); + + // Verify move to undefined stage after split screen activated moves task based on position. + when(mMainStage.isActive()).thenReturn(true); + assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); + mStageCoordinator.moveToStage(task, STAGE_TYPE_UNDEFINED, SPLIT_POSITION_TOP_OR_LEFT, + new WindowContainerTransaction()); + verify(mMainStage).addTask(eq(task), any(WindowContainerTransaction.class)); + assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); + } + + @Test + public void testDisplayAreaAppeared_initializesUnfoldControllers() { + mStageCoordinator.onDisplayAreaAppeared(mock(DisplayAreaInfo.class)); + + verify(mMainUnfoldController).init(); + verify(mSideUnfoldController).init(); + } + + @Test + public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() { + mStageCoordinator = createStageCoordinator(mSplitLayout); + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); + clearInvocations(mMainUnfoldController, mSideUnfoldController); + + mStageCoordinator.onLayoutSizeChanged(mSplitLayout); + + verify(mMainUnfoldController).onLayoutChanged(mBounds2); + verify(mSideUnfoldController).onLayoutChanged(mBounds1); + } + + @Test + public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() { + mStageCoordinator = createStageCoordinator(mSplitLayout); + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null); + clearInvocations(mMainUnfoldController, mSideUnfoldController); + + mStageCoordinator.onLayoutSizeChanged(mSplitLayout); + + verify(mMainUnfoldController).onLayoutChanged(mBounds1); + verify(mSideUnfoldController).onLayoutChanged(mBounds2); } @Test @@ -90,4 +194,119 @@ public class StageCoordinatorTests extends ShellTestCase { verify(mSideStage).removeTask( eq(task.taskId), any(), any(WindowContainerTransaction.class)); } + + @Test + public void testExitSplitScreen() { + when(mMainStage.isActive()).thenReturn(true); + mStageCoordinator.exitSplitScreen(INVALID_TASK_ID, EXIT_REASON_RETURN_HOME); + verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false)); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); + } + + @Test + public void testExitSplitScreenToMainStage() { + when(mMainStage.isActive()).thenReturn(true); + final int testTaskId = 12345; + when(mMainStage.containsTask(eq(testTaskId))).thenReturn(true); + when(mSideStage.containsTask(eq(testTaskId))).thenReturn(false); + mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME); + verify(mMainStage).reorderChild(eq(testTaskId), eq(true), + any(WindowContainerTransaction.class)); + verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(false)); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(true)); + } + + @Test + public void testExitSplitScreenToSideStage() { + when(mMainStage.isActive()).thenReturn(true); + final int testTaskId = 12345; + when(mMainStage.containsTask(eq(testTaskId))).thenReturn(false); + when(mSideStage.containsTask(eq(testTaskId))).thenReturn(true); + mStageCoordinator.exitSplitScreen(testTaskId, EXIT_REASON_RETURN_HOME); + verify(mSideStage).reorderChild(eq(testTaskId), eq(true), + any(WindowContainerTransaction.class)); + verify(mSideStage).removeAllTasks(any(WindowContainerTransaction.class), eq(true)); + verify(mMainStage).deactivate(any(WindowContainerTransaction.class), eq(false)); + } + + @Test + public void testResolveStartStage_beforeSplitActivated_setsStagePosition() { + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, SPLIT_POSITION_BOTTOM_OR_RIGHT, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getSideStagePosition(), SPLIT_POSITION_BOTTOM_OR_RIGHT); + verify(mStageCoordinator).updateActivityOptions(any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT)); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, SPLIT_POSITION_TOP_OR_LEFT, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getSideStagePosition(), SPLIT_POSITION_TOP_OR_LEFT); + verify(mStageCoordinator).updateActivityOptions(any(), eq(SPLIT_POSITION_TOP_OR_LEFT)); + } + + @Test + public void testResolveStartStage_afterSplitActivated_retrievesStagePosition() { + when(mMainStage.isActive()).thenReturn(true); + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, SPLIT_POSITION_TOP_OR_LEFT, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getSideStagePosition(), SPLIT_POSITION_TOP_OR_LEFT); + verify(mStageCoordinator).updateActivityOptions(any(), eq(SPLIT_POSITION_TOP_OR_LEFT)); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, SPLIT_POSITION_BOTTOM_OR_RIGHT, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getMainStagePosition(), SPLIT_POSITION_BOTTOM_OR_RIGHT); + verify(mStageCoordinator).updateActivityOptions(any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT)); + } + + @Test + public void testResolveStartStage_setsSideStagePosition() { + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_SIDE, SPLIT_POSITION_BOTTOM_OR_RIGHT, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getSideStagePosition(), SPLIT_POSITION_BOTTOM_OR_RIGHT); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, SPLIT_POSITION_BOTTOM_OR_RIGHT, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getMainStagePosition(), SPLIT_POSITION_BOTTOM_OR_RIGHT); + } + + @Test + public void testResolveStartStage_retrievesStagePosition() { + mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null /* wct */); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_SIDE, SPLIT_POSITION_UNDEFINED, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getSideStagePosition(), SPLIT_POSITION_TOP_OR_LEFT); + + mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN, SPLIT_POSITION_UNDEFINED, + null /* options */, null /* wct */); + assertEquals(mStageCoordinator.getMainStagePosition(), SPLIT_POSITION_BOTTOM_OR_RIGHT); + } + + private StageCoordinator createStageCoordinator(SplitLayout splitLayout) { + return new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, + mSyncQueue, mRootTDAOrganizer, mTaskOrganizer, mMainStage, mSideStage, + mDisplayImeController, mDisplayInsetsController, splitLayout, + mTransitions, mTransactionPool, mLogger, Optional.empty(), + new UnfoldControllerProvider()); + } + + private class UnfoldControllerProvider implements + Provider<Optional<StageTaskUnfoldController>> { + + private boolean isMain = true; + + @Override + public Optional<StageTaskUnfoldController> get() { + if (isMain) { + isMain = false; + return Optional.of(mMainUnfoldController); + } else { + return Optional.of(mSideUnfoldController); + } + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java index 90b5b37694c6..53d5076f5835 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -21,18 +21,27 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.app.ActivityManager; +import android.os.SystemProperties; import android.view.SurfaceControl; import android.view.SurfaceSession; +import android.window.WindowContainerTransaction; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; import com.android.wm.shell.common.SyncTransactionQueue; @@ -47,31 +56,48 @@ import org.mockito.MockitoAnnotations; /** * Tests for {@link StageTaskListener} * Build/Install/Run: - * atest WMShellUnitTests:StageTaskListenerTests + * atest WMShellUnitTests:StageTaskListenerTests */ @SmallTest @RunWith(AndroidJUnit4.class) -public final class StageTaskListenerTests { - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private StageTaskListener.StageListenerCallbacks mCallbacks; - @Mock private SyncTransactionQueue mSyncQueue; - @Captor private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor; +public final class StageTaskListenerTests extends ShellTestCase { + private static final boolean ENABLE_SHELL_TRANSITIONS = + SystemProperties.getBoolean("persist.debug.shell_transit", false); + + @Mock + private ShellTaskOrganizer mTaskOrganizer; + @Mock + private StageTaskListener.StageListenerCallbacks mCallbacks; + @Mock + private SyncTransactionQueue mSyncQueue; + @Mock + private IconProvider mIconProvider; + @Mock + private StageTaskUnfoldController mStageTaskUnfoldController; + @Captor + private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor; private SurfaceSession mSurfaceSession = new SurfaceSession(); + private SurfaceControl mSurfaceControl; private ActivityManager.RunningTaskInfo mRootTask; private StageTaskListener mStageTaskListener; @Before + @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); mStageTaskListener = new StageTaskListener( + mContext, mTaskOrganizer, DEFAULT_DISPLAY, mCallbacks, mSyncQueue, - mSurfaceSession); + mSurfaceSession, + mIconProvider, + mStageTaskUnfoldController); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootTask.parentTaskId = INVALID_TASK_ID; - mStageTaskListener.onTaskAppeared(mRootTask, new SurfaceControl()); + mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build(); + mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl); } @Test @@ -93,15 +119,39 @@ public final class StageTaskListenerTests { @Test public void testChildTaskAppeared() { + // With shell transitions, the transition manages status changes, so skip this test. + assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo childTask = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); - mStageTaskListener.onTaskAppeared(childTask, new SurfaceControl()); + mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl); assertThat(mStageTaskListener.mChildrenTaskInfo.contains(childTask.taskId)).isTrue(); verify(mCallbacks).onStatusChanged(eq(mRootTask.isVisible), eq(true)); } + @Test + public void testTaskAppeared_notifiesUnfoldListener() { + final ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + + mStageTaskListener.onTaskAppeared(task, mSurfaceControl); + + verify(mStageTaskUnfoldController).onTaskAppeared(eq(task), eq(mSurfaceControl)); + } + + @Test + public void testTaskVanished_notifiesUnfoldListener() { + final ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + mStageTaskListener.onTaskAppeared(task, mSurfaceControl); + clearInvocations(mStageTaskUnfoldController); + + mStageTaskListener.onTaskVanished(task); + + verify(mStageTaskUnfoldController).onTaskVanished(eq(task)); + } + @Test(expected = IllegalArgumentException.class) public void testUnknownTaskVanished() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); @@ -110,6 +160,8 @@ public final class StageTaskListenerTests { @Test public void testTaskVanished() { + // With shell transitions, the transition manages status changes, so skip this test. + assumeFalse(ENABLE_SHELL_TRANSITIONS); final ActivityManager.RunningTaskInfo childTask = new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); mStageTaskListener.mRootTaskInfo = mRootTask; @@ -131,4 +183,18 @@ public final class StageTaskListenerTests { mStageTaskListener.onTaskInfoChanged(childTask); verify(mCallbacks).onNoLongerSupportMultiWindow(); } + + @Test + public void testEvictAllChildren() { + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mStageTaskListener.evictAllChildren(wct); + assertTrue(wct.isEmpty()); + + final ActivityManager.RunningTaskInfo childTask = + new TestRunningTaskInfoBuilder().setParentTaskId(mRootTask.taskId).build(); + mStageTaskListener.onTaskAppeared(childTask, mSurfaceControl); + + mStageTaskListener.evictAllChildren(wct); + assertFalse(wct.isEmpty()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java index 18b8faf4dbe6..d92b12e60780 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java @@ -31,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -53,21 +54,23 @@ import android.os.IBinder; import android.os.Looper; import android.os.UserHandle; import android.testing.TestableContext; +import android.view.Display; import android.view.IWindowSession; import android.view.InsetsState; import android.view.Surface; -import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; import android.window.StartingWindowInfo; +import android.window.StartingWindowRemovalInfo; import android.window.TaskSnapshot; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; @@ -92,6 +95,8 @@ public class StartingSurfaceDrawerTests { @Mock private WindowManager mMockWindowManager; @Mock + private IconProvider mIconProvider; + @Mock private TransactionPool mTransactionPool; private final Handler mTestHandler = new Handler(Looper.getMainLooper()); @@ -104,26 +109,28 @@ public class StartingSurfaceDrawerTests { int mAddWindowForTask = 0; TestStartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor, - TransactionPool pool) { - super(context, splashScreenExecutor, pool); + IconProvider iconProvider, TransactionPool pool) { + super(context, splashScreenExecutor, iconProvider, pool); } @Override - protected boolean addWindow(int taskId, IBinder appToken, - View view, WindowManager wm, WindowManager.LayoutParams params, int suggestType) { + protected boolean addWindow(int taskId, IBinder appToken, View view, Display display, + WindowManager.LayoutParams params, int suggestType) { // listen for addView mAddWindowForTask = taskId; + saveSplashScreenRecord(appToken, taskId, view, suggestType); // Do not wait for background color return false; } @Override - protected void removeWindowSynced(int taskId, SurfaceControl leash, Rect frame, - boolean playRevealAnimation) { + protected void removeWindowSynced(StartingWindowRemovalInfo removalInfo, + boolean immediately) { // listen for removeView - if (mAddWindowForTask == taskId) { + if (mAddWindowForTask == removalInfo.taskId) { mAddWindowForTask = 0; } + mStartingWindowRecords.remove(removalInfo.taskId); } } @@ -156,7 +163,8 @@ public class StartingSurfaceDrawerTests { doNothing().when(mMockWindowManager).addView(any(), any()); mTestExecutor = new HandlerExecutor(mTestHandler); mStartingSurfaceDrawer = spy( - new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mTransactionPool)); + new TestStartingSurfaceDrawer(mTestContext, mTestExecutor, mIconProvider, + mTransactionPool)); } @Test @@ -171,9 +179,11 @@ public class StartingSurfaceDrawerTests { eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); - mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId, null, null, false); + StartingWindowRemovalInfo removalInfo = new StartingWindowRemovalInfo(); + removalInfo.taskId = windowInfo.taskInfo.taskId; + mStartingSurfaceDrawer.removeStartingWindow(removalInfo); waitHandlerIdle(mTestHandler); - verify(mStartingSurfaceDrawer).removeWindowSynced(eq(taskId), any(), any(), eq(false)); + verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(false)); assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, 0); } @@ -261,11 +271,32 @@ public class StartingSurfaceDrawerTests { // Verify the task snapshot with IME snapshot will be removed when received the real IME // drawn callback. + // makeTaskSnapshotWindow shall call removeWindowSynced before there add a new + // StartingWindowRecord for the task. mStartingSurfaceDrawer.onImeDrawnOnTask(1); - verify(mockSnapshotWindow).removeImmediately(); + verify(mStartingSurfaceDrawer, times(2)) + .removeWindowSynced(any(), eq(true)); } } + @Test + public void testClearAllWindows() { + final int taskId = 1; + final StartingWindowInfo windowInfo = + createWindowInfo(taskId, android.R.style.Theme); + mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, mBinder, + STARTING_WINDOW_TYPE_SPLASH_SCREEN); + waitHandlerIdle(mTestHandler); + verify(mStartingSurfaceDrawer).addWindow(eq(taskId), eq(mBinder), any(), any(), any(), + eq(STARTING_WINDOW_TYPE_SPLASH_SCREEN)); + assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); + + mStartingSurfaceDrawer.clearAllWindows(); + waitHandlerIdle(mTestHandler); + verify(mStartingSurfaceDrawer).removeWindowSynced(any(), eq(true)); + assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0); + } + private StartingWindowInfo createWindowInfo(int taskId, int themeResId) { StartingWindowInfo windowInfo = new StartingWindowInfo(); final ActivityInfo info = new ActivityInfo(); @@ -295,8 +326,8 @@ public class StartingSurfaceDrawerTests { System.currentTimeMillis(), new ComponentName("", ""), buffer, ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT, - Surface.ROTATION_0, taskSize, contentInsets, false, - true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, + Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, + false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */, hasImeSurface /* hasImeSurface */); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java index a098a6863493..78e27c956807 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/TaskSnapshotWindowTest.java @@ -83,8 +83,7 @@ public class TaskSnapshotWindowTest { createTaskDescription(Color.WHITE, Color.RED, Color.BLUE), 0 /* appearance */, windowFlags /* windowFlags */, 0 /* privateWindowFlags */, taskBounds, ORIENTATION_PORTRAIT, ACTIVITY_TYPE_STANDARD, - 100 /* delayRemovalTime */, new InsetsState(), - null /* clearWindow */, new TestShellExecutor()); + new InsetsState(), null /* clearWindow */, new TestShellExecutor()); } private TaskSnapshot createTaskSnapshot(int width, int height, Point taskSize, @@ -95,8 +94,8 @@ public class TaskSnapshotWindowTest { System.currentTimeMillis(), new ComponentName("", ""), buffer, ColorSpace.get(ColorSpace.Named.SRGB), ORIENTATION_PORTRAIT, - Surface.ROTATION_0, taskSize, contentInsets, false, - true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, + Surface.ROTATION_0, taskSize, contentInsets, new Rect() /* letterboxInsets */, + false, true /* isRealSnapshot */, WINDOWING_MODE_FULLSCREEN, 0 /* systemUiVisibility */, false /* isTranslucent */, false /* hasImeSurface */); } 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 2d2ab2c9f674..e39171343bb9 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 @@ -20,12 +20,20 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; +import static android.window.TransitionInfo.FLAG_IS_DISPLAY; +import static android.window.TransitionInfo.FLAG_TRANSLUCENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -48,10 +56,14 @@ import android.content.Context; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.view.IDisplayWindowListener; +import android.view.IWindowManager; +import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransition; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -65,17 +77,23 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.TestShellExecutor; +import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TransactionPool; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.util.ArrayList; /** * Tests for the shell transitions. + * + * Build/Install/Run: + * atest WMShellUnitTests:ShellTransitionTests */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -97,8 +115,7 @@ public class ShellTransitionTests { @Test public void testBasicTransitionFlow() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); IBinder transitToken = new Binder(); @@ -117,8 +134,7 @@ public class ShellTransitionTests { @Test public void testNonDefaultHandler() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final WindowContainerTransaction handlerWCT = new WindowContainerTransaction(); @@ -127,11 +143,13 @@ public class ShellTransitionTests { TestTransitionHandler testHandler = new TestTransitionHandler() { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { for (TransitionInfo.Change chg : info.getChanges()) { if (chg.getMode() == TRANSIT_CHANGE) { - return super.startAnimation(transition, info, t, finishCallback); + return super.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); } } return false; @@ -199,8 +217,7 @@ public class ShellTransitionTests { @Test public void testRequestRemoteTransition() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -211,7 +228,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(remoteFinishWCT); + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } @Override @@ -222,7 +239,8 @@ public class ShellTransitionTests { }; IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, - new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, testRemote)); + new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, + new RemoteTransition(testRemote))); verify(mOrganizer, times(1)).startTransition(eq(TRANSIT_OPEN), eq(transitToken), any()); TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); @@ -273,9 +291,76 @@ public class ShellTransitionTests { } @Test + public void testTransitionFilterNotRequirement() { + // filter that requires one opening and NO translucent apps + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = new TransitionFilter.Requirement[]{ + new TransitionFilter.Requirement(), new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + filter.mRequirements[1].mFlags = FLAG_TRANSLUCENT; + filter.mRequirements[1].mNot = true; + + final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertTrue(filter.matches(openOnly)); + + final TransitionInfo openAndTranslucent = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); + openAndTranslucent.getChanges().get(1).setFlags(FLAG_TRANSLUCENT); + assertFalse(filter.matches(openAndTranslucent)); + } + + @Test + public void testTransitionFilterChecksTypeSet() { + TransitionFilter filter = new TransitionFilter(); + filter.mTypeSet = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + final TransitionInfo openOnly = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertTrue(filter.matches(openOnly)); + + final TransitionInfo toFrontOnly = new TransitionInfoBuilder(TRANSIT_TO_FRONT) + .addChange(TRANSIT_TO_FRONT).build(); + assertTrue(filter.matches(toFrontOnly)); + + final TransitionInfo closeOnly = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_CLOSE).build(); + assertFalse(filter.matches(closeOnly)); + } + + @Test + public void testTransitionFilterChecksFlags() { + TransitionFilter filter = new TransitionFilter(); + filter.mFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; + + final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK, + TRANSIT_FLAG_KEYGUARD_GOING_AWAY) + .addChange(TRANSIT_TO_BACK).build(); + assertTrue(filter.matches(withFlag)); + + final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertFalse(filter.matches(withoutFlag)); + } + + @Test + public void testTransitionFilterChecksNotFlags() { + TransitionFilter filter = new TransitionFilter(); + filter.mNotFlags = TRANSIT_FLAG_KEYGUARD_GOING_AWAY; + + final TransitionInfo withFlag = new TransitionInfoBuilder(TRANSIT_TO_BACK, + TRANSIT_FLAG_KEYGUARD_GOING_AWAY) + .addChange(TRANSIT_TO_BACK).build(); + assertFalse(filter.matches(withFlag)); + + final TransitionInfo withoutFlag = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN).build(); + assertTrue(filter.matches(withoutFlag)); + } + + @Test public void testRegisteredRemoteTransition() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -285,7 +370,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(null /* wct */); + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); } @Override @@ -300,7 +385,7 @@ public class ShellTransitionTests { new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; - transitions.registerRemote(filter, testRemote); + transitions.registerRemote(filter, new RemoteTransition(testRemote)); mMainExecutor.flushAll(); IBinder transitToken = new Binder(); @@ -320,8 +405,7 @@ public class ShellTransitionTests { @Test public void testOneShotRemoteHandler() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; @@ -332,7 +416,7 @@ public class ShellTransitionTests { SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { remoteCalled[0] = true; - finishCallback.onTransitionFinished(remoteFinishWCT); + finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } @Override @@ -344,11 +428,12 @@ public class ShellTransitionTests { final int transitType = TRANSIT_FIRST_CUSTOM + 1; - OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, testRemote); + OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor, + new RemoteTransition(testRemote)); // Verify that it responds to the remote but not other things. IBinder transitToken = new Binder(); assertNotNull(oneShot.handleRequest(transitToken, - new TransitionRequestInfo(transitType, null, testRemote))); + new TransitionRequestInfo(transitType, null, new RemoteTransition(testRemote)))); assertNull(oneShot.handleRequest(transitToken, new TransitionRequestInfo(transitType, null, null))); @@ -358,15 +443,16 @@ public class ShellTransitionTests { oneShot.setTransition(transitToken); IBinder anotherToken = new Binder(); assertFalse(oneShot.startAnimation(anotherToken, new TransitionInfo(transitType, 0), - mock(SurfaceControl.Transaction.class), testFinish)); + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + testFinish)); assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0), - mock(SurfaceControl.Transaction.class), testFinish)); + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + testFinish)); } @Test public void testTransitionQueueing() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); IBinder transitToken1 = new Binder(); @@ -406,8 +492,7 @@ public class ShellTransitionTests { @Test public void testTransitionMerging() { - Transitions transitions = new Transitions(mOrganizer, mTransactionPool, mContext, - mMainExecutor, mAnimExecutor); + Transitions transitions = createTestTransitions(); mDefaultHandler.setSimulateMerge(true); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -443,11 +528,80 @@ public class ShellTransitionTests { assertEquals(0, mDefaultHandler.activeCount()); } + @Test + public void testShouldRotateSeamlessly() throws Exception { + final RunningTaskInfo taskInfo = + createTaskInfo(1, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); + final RunningTaskInfo taskInfoPip = + createTaskInfo(1, WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); + + final DisplayController displays = createTestDisplayController(); + final @Surface.Rotation int upsideDown = displays + .getDisplayLayout(DEFAULT_DISPLAY).getUpsideDownRotation(); + + final TransitionInfo normalDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() + .build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo).setRotate().build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(normalDispRotate, displays)); + + // Seamless if all tasks are seamless + final TransitionInfo rotateSeamless = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() + .build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertTrue(DefaultTransitionHandler.isRotationSeamless(rotateSeamless, displays)); + + // Not seamless if there is PiP (or any other non-seamless task) + final TransitionInfo pipDispRotate = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY).setRotate() + .build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfoPip) + .setRotate().build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(pipDispRotate, displays)); + + // Not seamless if one of rotations is upside-down + final TransitionInfo seamlessUpsideDown = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate(upsideDown, ROTATION_ANIMATION_UNSPECIFIED).build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(upsideDown, ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessUpsideDown, displays)); + + // Not seamless if system alert windows + final TransitionInfo seamlessButAlert = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags( + FLAG_IS_DISPLAY | FLAG_DISPLAY_HAS_ALERT_WINDOWS).setRotate().build()) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setTask(taskInfo) + .setRotate(ROTATION_ANIMATION_SEAMLESS).build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(seamlessButAlert, displays)); + + // Not seamless if there is no changed task. + final TransitionInfo noTask = new TransitionInfoBuilder(TRANSIT_CHANGE) + .addChange(new ChangeBuilder(TRANSIT_CHANGE).setFlags(FLAG_IS_DISPLAY) + .setRotate().build()) + .build(); + assertFalse(DefaultTransitionHandler.isRotationSeamless(noTask, displays)); + } + class TransitionInfoBuilder { final TransitionInfo mInfo; TransitionInfoBuilder(@WindowManager.TransitionType int type) { - mInfo = new TransitionInfo(type, 0 /* flags */); + this(type, 0 /* flags */); + } + + TransitionInfoBuilder(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags) { + mInfo = new TransitionInfo(type, flags); mInfo.setRootLeash(createMockSurface(true /* valid */), 0, 0); } @@ -465,11 +619,53 @@ public class ShellTransitionTests { return addChange(mode, null /* taskInfo */); } + TransitionInfoBuilder addChange(TransitionInfo.Change change) { + mInfo.addChange(change); + return this; + } + TransitionInfo build() { return mInfo; } } + class ChangeBuilder { + final TransitionInfo.Change mChange; + + ChangeBuilder(@WindowManager.TransitionType int mode) { + mChange = new TransitionInfo.Change(null /* token */, null /* leash */); + mChange.setMode(mode); + } + + ChangeBuilder setFlags(@TransitionInfo.ChangeFlags int flags) { + mChange.setFlags(flags); + return this; + } + + ChangeBuilder setTask(RunningTaskInfo taskInfo) { + mChange.setTaskInfo(taskInfo); + return this; + } + + ChangeBuilder setRotate(int anim) { + return setRotate(Surface.ROTATION_90, anim); + } + + ChangeBuilder setRotate() { + return setRotate(ROTATION_ANIMATION_UNSPECIFIED); + } + + ChangeBuilder setRotate(@Surface.Rotation int target, int anim) { + mChange.setRotation(Surface.ROTATION_0, target); + mChange.setRotationAnimation(anim); + return this; + } + + TransitionInfo.Change build() { + return mChange; + } + } + class TestTransitionHandler implements Transitions.TransitionHandler { ArrayList<Transitions.TransitionFinishCallback> mFinishes = new ArrayList<>(); final ArrayList<IBinder> mMerged = new ArrayList<>(); @@ -477,7 +673,8 @@ public class ShellTransitionTests { @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { mFinishes.add(finishCallback); return true; @@ -540,4 +737,34 @@ public class ShellTransitionTests { return taskInfo; } + private DisplayController createTestDisplayController() { + IWindowManager mockWM = mock(IWindowManager.class); + final IDisplayWindowListener[] displayListener = new IDisplayWindowListener[1]; + try { + doReturn(new int[] {DEFAULT_DISPLAY}).when(mockWM).registerDisplayWindowListener(any()); + } catch (RemoteException e) { + // No remote stuff happening, so this can't be hit + } + DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor); + out.initialize(); + return out; + } + + private Transitions createTestTransitions() { + return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(), + mContext, mMainExecutor, mAnimExecutor); + } +// +// private class TestDisplayController extends DisplayController { +// private final DisplayLayout mTestDisplayLayout; +// TestDisplayController() { +// super(mContext, mock(IWindowManager.class), mMainExecutor); +// mTestDisplayLayout = new DisplayLayout(); +// mTestDisplayLayout. +// } +// +// @Override +// DisplayLayout +// } + } |